/ Tags: RUBY 3 / Categories: RUBY

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).owner combined with .super_method is 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, :save rebinds names and breaks when applied twice. A prepended module with super composes cleanly, is visible in ancestors, and unwinds in the right order.
  • extend self and module_function differ. extend self puts the module in its own eigenclass, so the methods are both instance methods and module methods, sharing one definition. module_function creates private copies. The first is usually what you want for a stateless utility module.
  • Singleton methods block class-level changes. Reopening Dog and redefining speak won’t affect an object that already has its own speak. 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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More