Introspection and Reflection in Ruby

· 7 min read · Updated March 27, 2026 · intermediate
ruby metaprogramming introspection reflection

Ruby’s object model is unusually transparent. Because classes are themselves objects, and everything from strings to integers to modules is an object, you can ask any object about itself at runtime. This capability falls into two distinct categories: introspection (examining what’s there) and reflection (changing or invoking what’s there). Understanding the difference matters, and knowing which tools to reach for will save you from reaching for eval when you don’t need it.

Introspection: Asking What’s There

Introspection is read-only discovery. You inspect an object’s structure without changing anything. This is the mode you want for debugging, serialization, building admin panels, or any tool that works with objects it didn’t write.

Discovering Methods with methods and instance_methods

The most common introspection task is asking “what can this object do?” Every object in Ruby responds to methods, which returns every public method callable on it.

"hello".methods
# => [:+, :*, :split, :gsub, :upcase, :downcase, :to_s, :inspect, ...]

"hello".methods.include?(:upcase)
# => true

You can narrow the scope with protected_methods and private_methods, but often you’re interested in what a class or module provides as instance methods. That’s Module#instance_methods.

Array.instance_methods
# => [:[], :<<, :push, :pop, :map, :select, :first, :last, :empty?, ...]

Array.instance_methods(false)
# => [:[], :<<, :push, :pop, :first, :last, :empty?]  # only Array's own methods

Passing false to instance_methods excludes inherited methods, giving you only what that specific class defined. This distinction matters when you’re building something that needs to know the “own” methods of a class, not the full ancestral interface.

Checking What an Object Responds To

Before you call a method, especially when working with duck-typed APIs, you want to know if it’s safe. Object#respond_to? answers that directly.

[1, 2, 3].respond_to?(:map)
# => true

[1, 2, 3].respond_to?(:to_s)
# => true

obj.respond_to?(:process)  # do something conditional on this

By default respond_to? only checks public methods. Pass true as the second argument to include private methods in the check.

[1, 2, 3].respond_to?(:to_s, true)
# => true  (Object#to_s is a private method on Array's ancestor)

Inspecting Instance Variables

An object’s state lives in its instance variables. Object#instance_variables lists what’s currently set on an instance.

class Dog
  def initialize(name, breed)
    @name = name
    @breed = breed
  end
end

buddy = Dog.new("Buddy", "Labrador")
buddy.instance_variables
# => [:@name, :@breed]

One gotcha: only assigned instance variables appear. If you define an attr_accessor but never assign to it, that variable won’t show up in instance_variables. This matters for ORMs and serializers that try to persist “all” fields.

Module#class_variables does the same for class-level variables:

class Vehicle
  @@wheels = 4
end

class Car < Vehicle
  @@doors = 4
end

Car.class_variables
# => [:@@doors, :@@wheels]

Class variables are inherited, so Car.class_variables includes @@wheels from Vehicle.

Constants with Module#constants

Every module holds constants. Module#constants returns them as an array of symbols.

Math.constants
# => [:PI, :E]

JSON.constants(false)
# => [:STATE_TABLE, :VERSION]  # only what JSON itself defined

Pass false to exclude constants from included modules. This is useful when you want the “own” constants of a module without the noise from Kernel or Object.

The Ancestor Chain with Class#ancestors

Understanding Ruby’s method lookup requires knowing the ancestor chain. Class#ancestors walks the inheritance tree and included modules.

String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]

This is critical for understanding what will happen when you call a method. If String#upcase doesn’t exist on String itself, Ruby looks at Comparable, then Object, then Kernel, and finally BasicObject. This chain is also what makes mixins work: Comparable is a module, not a class, but it appears in String’s ancestor chain because String includes it.

Reflection: Changing and Invoking at Runtime

Reflection goes beyond asking questions. It lets you invoke methods, read and write constants, and even define new behavior dynamically. This is where Ruby’s metaprogramming gets its real power.

Calling Methods with Object#send

The most common reflection tool is Object#send. It invokes a method by name, accepting a symbol or string and any arguments.

class Calculator
  def add(a, b)
    a + b
  end

  private
  def secret_formula(n)
    n * 42
  end
end

calc = Calculator.new
calc.send(:add, 3, 4)
# => 7

calc.send(:secret_formula, 10)
# => 420  (send bypasses private visibility — use carefully)

Because send works with method names as values, it’s the bridge between introspection (finding that a method exists) and actually calling it. Serialization libraries, test doubles, and RPC frameworks all depend on this. The private method access is worth noting: send will call private methods that normal dot-notation cannot. That’s powerful and dangerous in equal measure.

Reading and Writing Constants Dynamically

Module#const_get retrieves a constant by name within a module’s scope:

Module.const_get(:String)
# => String

Math.const_get(:PI)
# => 3.141592653589793

Rails’ autoloader uses this to look up classes from configuration strings — when you reference a class name as a string, const_get resolves it.

Module#const_set does the opposite: it creates or overwrites a constant on a module:

MyModule = Module.new
MyModule.const_set(:DEFAULT_LIMIT, 100)
MyModule::DEFAULT_LIMIT
# => 100

Code generation tools use const_set to build classes dynamically. This is how ORMs create association methods, how service objects register themselves with a container, and how testing frameworks build mock classes on the fly.

Checking What Exists Before Acting

Module#method_defined? returns a boolean for whether a module or class defines a specific instance method:

String.method_defined?(:upcase)
# => true

String.method_defined?(:non_existent)
# => false

String.method_defined?(:upcase, false)
# => false  (String itself doesn't define it — it's inherited from Object)

Pass false as the second argument to exclude inherited methods. This is cleaner than comparing instance_methods(false) arrays when you just need a yes/no answer.

Introspection in Practice

Knowing these methods is one thing. Here’s where they actually show up in real Ruby code.

Serialization

YAML and JSON serialization libraries use introspection to convert any object to a hash or string representation without needing to know the class in advance.

require "json"

class Point
  attr_accessor :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def to_h
    # Introspection: find what "accessor" methods this class defines
    fields = self.class.instance_methods(false) & [:x, :y]
    Hash[fields.map { |f| [f, send(f)] }]
  end
end

Point.new(3, 4).to_h
# => {x: 3, y: 4}

The key here is using instance_methods(false) to find only the reader methods that were defined directly on Point, not inherited ones. Then send calls each one to get the current value.

Debugging Tools

Pry and byebug use introspection to show you the state of your program at a breakpoint. Here’s a simplified version of what Pry does when you inspect self:

def show_binding(binding)
  binding.local_variables.each do |var|
    value = binding.local_variable_get(var)
    puts "#{var} = #{value.inspect}"
  end
end

local_variable_get is itself a reflection method — it reads a local variable by name at runtime. local_variables lists all current local variables. Together they let you build a complete snapshot of the call site.

Flexible APIs with respond_to?

When you accept duck-typed objects, respond_to? guards against calling methods that don’t exist:

def process_item(item)
  if item.respond_to?(:to_h)
    # it's a hash-like object
    item.to_h.each { |k, v| puts "#{k}: #{v}" }
  elsif item.respond_to?(:each)
    # it's an enumerable
    item.each { |el| puts el }
  else
    puts item.to_s
  end
end

This pattern appears throughout the Ruby standard library and gems. Hash#merge, Array#concat, and many other methods accept either a hash or something that quacks like one.

Method Stubs in Tests

Testing frameworks like RSpec and Minitest use send and define_singleton_method to stub methods at runtime:

def stub(obj, method_name, return_value)
  original = obj.method(method_name)
  obj.define_singleton_method(method_name) { return_value }
  yield
ensure
  obj.define_singleton_method(method_name, original)
end

result = nil
stub([1, 2, 3], :sum, 999) do
  result = [1, 2, 3].sum
end

result
# => 999

[1, 2, 3].sum
# => 6  (original method restored)

define_singleton_method defines a method only on that specific object, not on the class. Combined with send, this is the mechanism behind RSpec’s allow(obj).to receive(:method).

The Introspection/Reflection Line

The distinction isn’t always crisp in practice, and that’s fine. send sits right on the boundary — you discover a method name via introspection, then invoke it via reflection. instance_eval straddles it too: it introspects to find the context, then reflects to change what self means inside a block.

Where the line matters is safety. Introspection is always safe. Reflection, especially eval and const_set, introduces risk of surprising behavior. eval in particular is a last resort — if you can solve something with send, use send.

# Bad: eval with user input — SQL injection analog for Ruby
eval(params[:code])

# Better: send with a known symbol
obj.send(params[:method].to_sym)  # still risky without allowlisting

See Also