Ruby's Object Model — Eigenclasses, Method Lookup, and What super Really Does
You can write Ruby for years without knowing what an eigenclass is. Then one day a gem defines a method you can’t find with grep, super calls something you didn’t expect, and define_method inside class << self behaves differently than the same call one line up. All of it follows from a single rule about how Ruby finds methods — and once that rule is clear, the surprises stop.
The One Rule
When you call object.some_method, Ruby walks a list called the ancestor chain, starting from the object’s singleton class, and calls the first matching method it finds. That’s the whole algorithm. Everything else is a question of what’s in the list and in what order.
Example:
class Animal
def speak = "..."
end
class Dog < Animal
def speak = "Woof"
end
Dog.ancestors
# => [Dog, Animal, Object, Kernel, BasicObject]
Dog.new.speak checks Dog first, finds speak, stops. It never looks at Animal. Inheritance isn’t a special mechanism — it’s just position in a list.
Singleton Classes (Eigenclasses)
Every object in Ruby can have methods that belong to it alone. Ruby stores them in a hidden class inserted directly above the object’s own class.
Example:
dog = Dog.new
def dog.speak
"Woof, but only from me"
end
dog.speak # => "Woof, but only from me"
Dog.new.speak # => "Woof"
dog.singleton_class # => #<Class:#<Dog:0x00007f...>>
dog.singleton_class.ancestors.first(3)
# => [#<Class:#<Dog:0x00007f...>>, Dog, Animal]
That anonymous #<Class:...> is the eigenclass. It sits between the object and its class, which is why the singleton method wins the lookup.
The class << obj syntax opens that hidden class directly:
Example:
dog = Dog.new
class << dog
def speak = "Woof, but only from me"
def fetch = "ball"
end
Identical result, and the block form lets you define several methods and use private, attr_reader, and friends inside.
Why Class Methods Are Just Singleton Methods
Here’s the part that makes the whole model click. A class is an object — an instance of Class. So a “class method” is nothing more than a singleton method on that object.
Example:
class Dog
def self.species = "Canis familiaris"
end
# Exactly equivalent:
class Dog
class << self
def species = "Canis familiaris"
end
end
Dog.singleton_class.instance_methods(false) # => [:species]
There is no separate “class method” concept in the object model. def self.foo defines an instance method on Dog’s eigenclass, and when you call Dog.foo, Ruby applies the same lookup rule it applies to everything else.
This also explains inherited class methods:
class Puppy < Dog; end
Puppy.species # => "Canis familiaris"
Puppy.singleton_class.ancestors.first(3)
# => [#<Class:Puppy>, #<Class:Dog>, #<Class:Animal>]
Eigenclasses inherit in parallel with their objects. Puppy’s eigenclass inherits from Dog’s eigenclass, so class methods flow down the hierarchy for free.
Where Modules Land
include, prepend, and extend each insert a module at a different point in the chain, and that position is the only thing that differs between them.
Example:
module Loud
def speak = super.upcase + "!!!"
end
class Dog < Animal
include Loud
def speak = "Woof"
end
Dog.ancestors
# => [Dog, Loud, Animal, Object, Kernel, BasicObject]
Dog.new.speak # => "Woof"
include puts the module below the class, so Dog#speak wins and Loud#speak is never reached. This surprises people constantly.
Example:
class Dog < Animal
prepend Loud
def speak = "Woof"
end
Dog.ancestors
# => [Loud, Dog, Animal, Object, Kernel, BasicObject]
Dog.new.speak # => "WOOF!!!"
prepend puts it above, so the module intercepts the call and its super reaches the class’s own definition. This is how you wrap a method without aliasing.
Example:
dog = Dog.new
dog.extend(Loud)
dog.singleton_class.ancestors.first(3)
# => [#<Class:#<Dog:0x...>>, Loud, Dog]
extend inserts the module into the object’s eigenclass, affecting that one object. And since classes are objects, Dog.extend(Loud) gives Dog a class method — which is precisely what extend self and the ClassMethods pattern in ActiveSupport::Concern are doing under the hood.
What super Actually Resolves To
super does not mean “call the parent class.” It means “continue the lookup from the current position in this object’s ancestor chain.”
That distinction matters the moment modules are involved.
Example:
module A
def call = "A -> #{super}"
end
module B
def call = "B -> #{super}"
end
class Base
def call = "Base"
end
class Child < Base
include A
include B
def call = "Child -> #{super}"
end
Child.ancestors
# => [Child, B, A, Base, Object, ...]
Child.new.call
# => "Child -> B -> A -> Base"
Note the ordering: the last include sits closest to the class. Each super moves exactly one step right in that list, regardless of whether the next entry is a module or a class.
Bare super vs super()
def call(arg)
super # passes arg along implicitly
super() # passes no arguments
super(arg.upcase) # passes what you specify
end
Bare super forwarding the original arguments is a frequent source of ArgumentError after a signature change in a parent. When the parent takes no arguments, write super() with the parens — the empty parens are meaningful.
Finding Where a Method Came From
When grep fails, ask the object.
Example:
m = dog.method(:speak)
m.owner # => Loud — the module or class that defines it
m.source_location # => ["/path/to/loud.rb", 3]
m.super_method # => #<Method: Dog#speak> — the next one in the chain
dog.singleton_class.ancestors # the full lookup order for this object
Dog.instance_method(:speak).owner
source_location returns nil for C-defined methods and for methods created by define_method with a block from a class_eval string — that nil is itself a diagnosis: something generated this method at runtime.
Pro-Tip:
method(:name).ownercombined with.super_methodis the fastest way to untangle a Rails method you can’t find. Chain it in a loop and you get the full override stack for one specific method — every gem, concern, and patch that touches it, in resolution order.m = obj.method(:save); while m; puts "#{m.owner} #{m.source_location}"; m = m.super_method; end. Run that in a console the next time an ActiveRecord callback behaves oddly and you’ll see exactly which of the eleven modules in the chain is responsible, in about four seconds. It beats reading source, and unlike reading source it reflects what’s actually loaded in the running process.
Practical Consequences
Three things follow from the model that are worth internalising:
- Prepend beats alias chaining.
alias_method :old_save, :saverebinds names and breaks when applied twice. A prepended module withsupercomposes cleanly, is visible inancestors, and unwinds in the right order. extend selfandmodule_functiondiffer.extend selfputs the module in its own eigenclass, so the methods are both instance methods and module methods, sharing one definition.module_functioncreates private copies. The first is usually what you want for a stateless utility module.- Singleton methods block class-level changes. Reopening
Dogand redefiningspeakwon’t affect an object that already has its ownspeak. If a patch “doesn’t take,” check the eigenclass first.
Conclusion
The object model is one rule — walk the ancestor chain, take the first match — plus a clear account of what’s in the chain: the eigenclass, prepended modules, the class, included modules, then upward. Class methods are eigenclass instance methods. super steps one position right, not one class up. Once those are solid, metaprogrammed code stops being mysterious and ancestors becomes the debugging tool you reach for first, because it shows you exactly the list Ruby is about to search.
FAQs
Q1: Do all objects have a singleton class?
Almost. Integers, symbols, nil, true, and false are immediates and cannot have singleton methods — calling singleton_class on an Integer raises TypeError. Everything else can.
Q2: What’s the difference between instance_methods and methods?
Klass.instance_methods lists methods available to instances of the class. obj.methods lists methods callable on that specific object, including singleton methods and everything inherited. Pass false to either to exclude inherited entries.
Q3: Does prepend hurt performance?
It adds one entry to the ancestor chain, and Ruby caches method lookups per call site, so steady-state cost is negligible. Defining methods at runtime after the cache is warm is what invalidates caches — the chain depth itself is not the problem.
Q4: Why is extend sometimes described as “include into the singleton class”?
Because that’s literally the implementation. obj.extend(M) is equivalent to obj.singleton_class.include(M), which is why the module shows up in obj.singleton_class.ancestors.
Q5: Can I remove a method from the chain once it’s added?
You can undef_method or remove_method on a class you control, but you can’t un-include a module — Ruby has no uninclude. This is a good reason to prefer prepend with a conditional inside over conditionally including modules at load time.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀