Introspection and Reflection in Ruby
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
- /tutorials/ruby-method-missing-deep-dive — continuation of the Ruby Metaprogramming series
- /guides/ruby-metaprogramming-basics — dynamic method definition,
define_method, and runtime code evaluation - /tutorials/ruby-classes-and-objects — foundational understanding of how Ruby objects work before exploring their reflective capabilities