Inheritance
Inheritance lets one class (a subclass) acquire the behavior and structure of another class (its superclass), then override or extend specific pieces of it. It is Ruby’s primary tool for expressing an “is-a” relationship — a Dog is an Animal, a Car is a Vehicle — so you can write shared logic once and specialize only what differs. Understanding inheritance also means understanding how Ruby actually finds a method when you call one, which is one of the most useful pieces of “under the hood” knowledge in the language.
Overview / How Inheritance Works
Every class in Ruby, unless told otherwise, inherits from Object, which itself inherits from BasicObject. This is why methods like .class, .inspect, and .respond_to? are available on virtually anything — they live on Object or modules mixed into it, and every class you write sits below Object in the chain unless you explicitly inherit from something else.
You declare inheritance with the < operator: class Dog < Animal makes Dog a subclass of Animal. A Dog instance can call every public and protected method defined on Animal, as well as anything defined directly on Dog. If Dog defines a method with the same name as one on Animal, the Dog version overrides it — Ruby always prefers the method found first while walking up the chain.
That walk has a name: the ancestor chain (or method resolution order). When you call a method on an object, Ruby searches, in order: the object’s singleton class (for methods defined on that one specific object), the object’s class, any modules included into that class (searched in reverse order of inclusion, most recently included first), then the superclass and its included modules, and so on up to Object, the built-in Kernel module, and finally BasicObject. The first matching method definition wins. You can inspect this chain yourself by calling SomeClass.ancestors, which returns an array of classes and modules in exactly the order Ruby searches them.
Ruby only supports single inheritance — a class has exactly one direct superclass. To share behavior across unrelated class hierarchies, Ruby gives you modules mixed in with include (instance methods) or extend (class methods), which slot into the ancestor chain alongside real inheritance. This combination of single inheritance plus mixins is deliberate: it keeps the chain a simple, predictable line instead of the diamond-shaped mess multiple inheritance can create in other languages.
Syntax
class SubclassName < SuperclassName
def initialize(*args)
super(*args) # forwards to SuperclassName#initialize
# additional setup for SubclassName
end
def some_method
# override, optionally calling super to reuse the parent's version
end
end
< SuperclassName— declares which class this one inherits from; omit it and Ruby usesObjectimplicitly.super(no parentheses) — calls the same-named method on the superclass, automatically forwarding whatever arguments and block the current method received.super()(empty parentheses) — calls the superclass method with no arguments, even if the current method received some.super(a, b)— calls the superclass method with exactly the arguments you list.self.class— inside an instance method, returns the object’s actual class, which may be a subclass even when the method itself is defined on the superclass.
Examples
Example 1: Overriding a method
class Animal
attr_reader :name
def initialize(name)
@name = name
end
def speak
"#{name} makes a sound"
end
def to_s
"#{self.class.name}(#{name})"
end
end
class Dog < Animal
def speak
"#{name} barks"
end
end
class Cat < Animal
end
animals = [Dog.new("Rex"), Cat.new("Whiskers")]
animals.each { |a| puts a.speak }
puts animals.first
Output:
Rex barks
Whiskers makes a sound
Dog(Rex)
Dog overrides speak, so calling it on a Dog instance runs the subclass’s version. Cat defines no speak at all, so Ruby keeps walking up the ancestor chain and finds Animal#speak instead. Notice to_s, defined only on Animal, still reports "Dog" as the class name — self.class always reflects the object’s real class, not where the method happens to be defined.
Example 2: Extending behavior with super
class Vehicle
attr_reader :make, :model
def initialize(make:, model:)
@make = make
@model = model
end
def description
"#{make} #{model}"
end
end
class Car < Vehicle
attr_reader :doors
def initialize(make:, model:, doors:)
super(make: make, model: model)
@doors = doors
end
def description
"#{super} with #{doors} doors"
end
end
car = Car.new(make: "Toyota", model: "Corolla", doors: 4)
puts car.description
Output:
Toyota Corolla with 4 doors
Car#initialize calls super(make: make, model: model) to let Vehicle#initialize set up @make and @model, then adds its own @doors. In description, the bare super (no parentheses) forwards whatever arguments the current call received — here, none, which matches Vehicle#description‘s zero-argument signature — and its return value, "Toyota Corolla", is interpolated into the fuller string. This is the essence of extending rather than replacing behavior.
Example 3: Mixins and the ancestor chain
module Flyable
def fly
"#{name} soars through the sky"
end
end
class Bird
include Flyable
attr_reader :name
def initialize(name)
@name = name
end
end
class Eagle < Bird
end
eagle = Eagle.new("Aquila")
puts eagle.fly
puts Eagle.ancestors.first(4).inspect
Output:
Aquila soars through the sky
[Eagle, Bird, Flyable, Object]
Eagle defines no fly method itself, and neither does its direct superclass Bird — but Bird mixed in the Flyable module with include, which inserts Flyable into the ancestor chain directly above Bird. Eagle.ancestors makes that chain visible: Ruby checks Eagle, then Bird, then the included Flyable module, then Object, and so on, stopping at the first class or module that defines fly.
How It Works Step by Step
When you write eagle.fly, Ruby does not know or care that fly lives on a module three steps up the chain — it just walks the chain in order until it finds a match:
- 1. Check
eagle‘s singleton class (methods defined on this one object specifically, e.g. viadef eagle.fly). None here. - 2. Check
Eagleitself. Noflydefined. - 3. Check
Bird. Noflydefined directly onBird. - 4. Check modules included into
Bird, most recently included first.Flyabledefinesfly— match found, search stops.
If no match were found all the way up through Object, Kernel, and BasicObject, Ruby would raise NoMethodError — unless the class defines method_missing, which acts as a catch-all invoked only after the normal search fails, enabling dynamic method dispatch. The same search order applies to super: it resumes the search from the point right after where the currently executing method was found, not from the object’s own class again, which is why calling super inside an overridden method correctly reaches the next definition up the chain rather than re-triggering the override itself.
Common Mistakes
Mistake 1: Forgetting to call super in initialize
Subclasses often need their own initialize, but overriding it completely skips the superclass’s setup unless you explicitly call super.
class Animal
attr_reader :name
def initialize(name)
@name = name
end
end
class Dog < Animal
attr_reader :breed
def initialize(name, breed)
@breed = breed
end
end
rex = Dog.new("Rex", "Labrador")
puts rex.name.inspect
Output:
nil
Dog#initialize never calls super, so Animal#initialize never runs and @name is never set — rex.name returns nil instead of raising an error, which makes the bug easy to miss. The fix is to forward the shared argument to the parent:
class Dog < Animal
attr_reader :breed
def initialize(name, breed)
super(name)
@breed = breed
end
end
Mistake 2: Class variables shared across the whole hierarchy
A @@class_variable is shared by a class and every one of its subclasses — it is not scoped per-subclass, which surprises people expecting each subclass to track its own state.
class Animal
@@count = 0
def initialize
@@count += 1
end
def self.count
@@count
end
end
class Dog < Animal
end
class Cat < Animal
end
Dog.new
Dog.new
Cat.new
puts Dog.count
puts Cat.count
Output:
3
3
Both Dog.count and Cat.count report 3, because @@count is one variable for the entire Animal/Dog/Cat family. To get independent counters per class, use a class instance variable (a single @, on the class object itself) instead, which is naturally separate for each class:
class Animal
def self.count
@count ||= 0
end
def self.increment_count
@count = count + 1
end
def initialize
self.class.increment_count
end
end
class Dog < Animal
end
class Cat < Animal
end
Dog.new
Dog.new
Cat.new
puts Dog.count
puts Cat.count
Output:
2
1
Best Practices
- Reach for inheritance only for genuine “is-a” relationships; when you just want to share behavior across otherwise-unrelated classes, prefer a mixin module instead.
- Always call
superin an overriddeninitializeunless you deliberately want to skip the parent’s setup — and if you do skip it, comment why. - Keep the ancestor chain shallow. Two or three levels of inheritance are usually enough; deep hierarchies make method lookup hard to reason about.
- Avoid
@@class_variablesin any class meant to be subclassed; use class instance variables (@countwithself.methods) when state needs to live on the class itself. - When overriding a method, call
superto extend the parent’s behavior rather than duplicating its logic — duplicated logic drifts out of sync over time. - Use
SomeClass.ancestorsandSomeClass.superclasswhenever a method’s behavior is confusing — they make the search order concrete instead of guesswork. - Favor composition (passing in collaborator objects) over a deep inheritance tree when subclasses only share a little behavior and differ in most other ways.
Practice Exercises
- Define a
Shapeclass with anareamethod that returns0, and two subclasses,RectangleandCircle, that overrideareacorrectly for their geometry. Create one of each and print their areas. - Build a three-level hierarchy:
Employee→Manager→Director, where each level’sinitializecallssuperto add one new attribute (e.g.salary, thenteam_size, thenregion). Confirm aDirectorinstance has all three attributes set correctly. - Write a module
Describablewith adescribemethod that usesnameandself.class, include it in two unrelated classes, and printSomeClass.ancestorsfor each to see where the module lands in the chain.
Summary
- Inheritance (
class Sub < Super) lets a subclass reuse and override a superclass’s methods, modeling an “is-a” relationship. - Ruby resolves method calls by walking the ancestor chain: singleton class, class, included modules (most recent first), superclass, and so on up to
Object,Kernel, andBasicObject. super(bare) forwards the current method’s arguments and block;super()forwards none;super(args)forwards exactly what you specify.- Ruby supports only single inheritance; use modules with
include/extendto share behavior across unrelated hierarchies. - Forgetting
superin an overriddeninitializesilently skips the parent’s setup instead of raising an error. @@class_variablesare shared across an entire hierarchy; use class instance variables when subclasses need independent state.SomeClass.ancestorsandSomeClass.superclassare the best tools for making method lookup concrete when behavior is confusing.
