Skip to content

An object can list its methods and variables

Level: 301 · for a reader debugging a library object in irb and wanting to ask it, rather than the docs, what it can do

One line: methods, instance_methods(false), instance_variables, instance_method(:x).parameters, source_location, ancestors, constants, singleton_methods, local_variables, defined? and __method__ answer at run time what a Python programmer asks with dir, vars, inspect.signature, inspect.getsourcefile, __mro__, locals() and sys._getframe() — with one structural difference: Ruby's attr_reader values are methods, so every listing separates methods from variables, while Python's dir mixes data attributes and functions.

Ruby keeps every class, method and variable as an object you can question. obj.methods lists the public methods an object answers to (subtracting Object.instance_methods leaves the interesting ones); Klass.instance_methods(false) lists only what the class itself defined, and private_instance_methods(false) the private ones, initialize included. instance_variables is the list of @names set so far, and instance_variable_get reads one without a reader. A method is an object too: instance_method(:name) gives an UnboundMethod with parameters — every parameter with its kind, :req, :opt, :rest, :keyreq, :key, :keyrest, :block — an arity, an owner and a source_location. ancestors is the lookup chain, constants the names a module holds, singleton_methods the methods one object alone has. Inside a method, local_variables and __method__ describe the frame, and defined? is a keyword that reports what kind of thing a name is without evaluating it: "local-variable", "method", "constant", "instance-variable", "expression" — or nil. The program asks each question of a small Geometry::Point.

Verified output of introspection_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. point.methods - Object.instance_methods (sorted) [:<, :<=, :>, :>=, :area, :between?, :clamp, :configure, :move!, :x, :y]
 2. Point.instance_methods(false).sort             [:<=>, :area, :configure, :move!, :x, :y]
    Point.private_instance_methods(false).sort     [:initialize, :secret]
 3. point.instance_variables                       [:@x, :@y]
    instance_variable_get(:@x) for each            {"@x": 1, "@y": 2}
 4. instance_method(:configure).parameters         [[:req, :a], [:opt, :b], [:rest, :r], [:keyreq, :k], [:key, :d], [:keyrest, :o], [:block, :blk]]
    arity of configure / area / move!              -3 / 0 / 2
 5. instance_method(:area).source_location         introspection_rb.rb:20
 6. Point.ancestors                                [Geometry::Point, Comparable, Object, Kernel, BasicObject]
 7. Geometry.constants.sort                        [:ORIGIN, :Point]
    Geometry.const_source_location(:ORIGIN)        introspection_rb.rb:12
 8. Point.singleton_methods                        [:origin]
    after define_singleton_method: point.singleton_methods [:label]
 9. local_variables inside a method (a = 1; b = 2) [:a, :b]
10. defined?(a) local / String / Nope / puts / nope ["local-variable", "constant", nil, "method", nil]
    defined?(@x) / (a = 1) / 3 / self / $stdout    ["instance-variable", "assignment", "expression", "self", "global-variable"]
    defined?(yield) / (super) with no block, no parent [nil, nil]
11. __method__ inside whoami                       :whoami
12. method(:area).owner / method(:between?).owner  Geometry::Point / Comparable
    method_defined?(:area) / (:secret) / private_method_defined?(:secret) true / false / true
    instance_variable_defined?(:@x) / (:@z)        true / false
13. point.class / Point.class / Geometry.class     Geometry::Point / Class / Module
    Point.name / Point.singleton_class             "Geometry::Point" / #<Class:Geometry::Point>

Reading the output

Row 1 subtracts Object's methods and leaves what Point and Comparable added — note that x and y are there, because attr_reader defines methods, and so are <, between? and clamp, which Comparable derived from <=>. Row 2 narrows to what the class body itself defined, public and then private; initialize is always private. Row 3 lists the variables as @-prefixed Symbols and reads each one. Row 4 is the richest answer: parameters names every parameter with its kind, and arity summarises them (a negative arity means optional parameters; -3 here counts a and the required keyword k as two mandatory arguments). Row 5 gives the file and line of a method's definition — the basename is printed because the full path would differ between machines. Row 6 is the lookup chain the method lookup lesson walks. Row 7: constants lists both the constant and the nested class, and const_source_location says where the constant was assigned. Row 8: class methods are the class's singleton methods, and a method added to one object shows only on that object. Rows 9–11 look inward from inside a method: local_variables lists the frame's names, defined? classifies a local, a constant, a method call, a missing name, an instance variable, an assignment, a literal, self and a global, and answers nil for yield with no block and for super with no parent method; __method__ is the running method's name. Row 12: owner says which class or module a method came from, method_defined? sees public and protected methods only, and private_method_defined? the private ones. Row 13: an instance's class, a class's class (Class), a module's class (Module), a class's full name, and its singleton class, which prints as #<Class:Geometry::Point>.

Compared with Python

dir(point) (row 1) lists attributes and methods together, and x, y are instance attributes there, not methods; Ruby's list has only methods because Ruby has only methods — inspect.getmembers(point, inspect.ismethod) is the filter that gives back a methods-only list — bound dunder methods such as __init__ included, the _secret convention ignored, and the bound classmethod origin counted as a method. Python's "own methods of the class" is vars(Point) filtered with inspect.isfunction (row 2), and it lists _secret like the rest — a private method is a naming convention and no listing hides it. vars(point) is instance_variables and their values in one dictionary (row 3). inspect.signature (row 4) is parameters and arity together, printed as a signature string, and each parameter's kind — POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, KEYWORD_ONLY, VAR_KEYWORD — is the twin of Ruby's :req/:opt/:rest/:keyreq/:key/:keyrest; there is no :block kind because a callback is an ordinary parameter. inspect.getsourcefile and getsourcelines(...)[1] are source_location, and inspect.getsource goes one step further than Ruby can and returns the text of the definition (row 5), __mro__ is ancestors (row 6), and a namespace's names come from vars() again (row 7), since Python has no constants and no constants — ORIGIN is a class attribute that happens to be upper-case. Class methods are classmethod objects in the class dictionary, and a per-object method is a bound types.MethodType stored in the instance dictionary (row 8). locals() inside a method includes self (row 9) — and is read-only in spirit: writing into it has never reliably changed a variable, and Python 3.13 made that official (not machine-checked here). Row 10: there is no defined?; Python answers piecemeal with in locals(), hasattr, callable and a try/except NameError, and an assignment is a statement with no value to classify. sys._getframe().f_code.co_name is __method__ (row 11), __qualname__ and inspect.ismethod cover owner and method_defined? (row 12), and type() applied three times (row 13) shows that in Python a class, a namespace class and the metaclass are all instances of type, where Ruby distinguishes Class from Module.

Verified output of introspection_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. [n for n in dir(point) if not n.startswith('_')] ['area', 'configure', 'move', 'origin', 'x', 'y']
    inspect.getmembers(point, inspect.ismethod) names ['__init__', '__lt__', '_secret', 'area', 'configure', 'move', 'origin']
 2. own functions of Point (vars, minus dunders)   ['_secret', 'area', 'configure', 'move']
    _secret is listed like any other               one underscore is a naming convention
 3. vars(point)                                    {'x': 1, 'y': 2}
    getattr(point, name) for each                  {'x': 1, 'y': 2}
 4. inspect.signature(Point.configure)             (self, a, b=1, *r, k, d=2, **o)
    parameter kinds                                self:POSITIONAL_OR_KEYWORD a:POSITIONAL_OR_KEYWORD b:POSITIONAL_OR_KEYWORD r:VAR_POSITIONAL k:KEYWORD_ONLY d:KEYWORD_ONLY o:VAR_KEYWORD
 5. inspect.getsourcefile / getsourcelines(area)[1] introspection_py.py:27
    inspect.getsource(area).splitlines()[0]        def area(self):
 6. [c.__name__ for c in Point.__mro__]            ['Point', 'object']
 7. own names of Geometry                          ['ORIGIN', 'Point']
    inspect.getsourcelines(Geometry)[1]            15
 8. classmethods of Point                          ['origin']
    callables in vars(point) after MethodType      ['label']
 9. sorted(locals()) inside a method (a = 1; b = 2) ['a', 'b', 'self']
10. 'a' in locals() / str / Nope / print / nope    [True, True, None, True, None]
    hasattr(self, 'x') / assignment / 3 / self / stdout [True, 'assignment is a statement', 'defined', 'defined', True]
    no yield/super probes                          a callback is a parameter; super() is explicit
11. sys._getframe().f_code.co_name inside whoami   'whoami'
12. Point.area.__qualname__ / inspect.ismethod(point.area) Geometry.Point.area / True
    hasattr(Point, 'area') / ('_secret') / in vars(Point) True / True / True
    'x' in vars(point) / 'z' in vars(point)        True / False
13. type(point) / type(Point) / type(Geometry)     Point / type / type
    Point.__qualname__ / Point.__module__          'Geometry.Point' / '__main__'

The inspect-style listing of an object's repr values leans on the repr/str split, which the Python library measures on its own page: repr is not str ↗.

Try it

  1. Write describe(obj) that prints the object's class, its ancestors up to Object, every instance variable with its value, and every method the class itself defines with each parameter's name and kind; run it on a small Account class that has an optional argument, a keyword argument, a splat and a **options.
  2. Write where_defined(obj, name) that returns "#{File.basename(file)}:#{line}" for a method, using method(name).source_location — then try it on obj.method(:puts) and on [].method(:map). What does source_location return for a method written in C, and how would you tell such a method apart from one defined with define_method from a block?
Solution to 1

introspection_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# introspection_kata_rb.rb — describe(obj): class, own methods with parameters, instance variables.

class Account
  include Comparable
  attr_reader :owner, :balance

  def initialize(owner, balance = 0) = (@owner, @balance = owner, balance)
  def deposit(amount, note: nil) = (@balance += amount; self)
  def withdraw(amount, *fees, **options) = (@balance -= amount + fees.sum; self)
  def <=>(other) = balance <=> other.balance
end

def describe(obj)
  klass = obj.class
  puts "class:     #{klass}"
  puts "ancestors: #{klass.ancestors.take_while { |a| a != Object }.inspect}"
  puts "ivars:     " + obj.instance_variables.map { |v| "#{v} = #{obj.instance_variable_get(v).inspect}" }.join(", ")
  puts "methods:"
  klass.instance_methods(false).sort.each do |name|
    params = klass.instance_method(name).parameters.map { |kind, pname| "#{pname}:#{kind}" }
    puts format("  %-10s (%s)", name, params.join(", "))
  end
end

describe(Account.new("Ada", 100).deposit(50, note: "gift"))

Verified output of introspection_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

class:     Account
ancestors: [Account, Comparable]
ivars:     @owner = "Ada", @balance = 150
methods:
  <=>        (other:req)
  balance    ()
  deposit    (amount:req, note:key)
  owner      ()
  withdraw   (amount:req, fees:rest, options:keyrest)

See also