Inheritance

Published by Programming Line in Ruby. Last updated .

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 uses Object implicitly.
  • 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. via def eagle.fly). None here.
  • 2. Check Eagle itself. No fly defined.
  • 3. Check Bird. No fly defined directly on Bird.
  • 4. Check modules included into Bird, most recently included first. Flyable defines fly — 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 super in an overridden initialize unless 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_variables in any class meant to be subclassed; use class instance variables (@count with self. methods) when state needs to live on the class itself.
  • When overriding a method, call super to extend the parent’s behavior rather than duplicating its logic — duplicated logic drifts out of sync over time.
  • Use SomeClass.ancestors and SomeClass.superclass whenever 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 Shape class with an area method that returns 0, and two subclasses, Rectangle and Circle, that override area correctly for their geometry. Create one of each and print their areas.
  • Build a three-level hierarchy: Employee → Manager → Director, where each level’s initialize calls super to add one new attribute (e.g. salary, then team_size, then region). Confirm a Director instance has all three attributes set correctly.
  • Write a module Describable with a describe method that uses name and self.class, include it in two unrelated classes, and print SomeClass.ancestors for 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, and BasicObject.
  • 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/extend to share behavior across unrelated hierarchies.
  • Forgetting super in an overridden initialize silently skips the parent’s setup instead of raising an error.
  • @@class_variables are shared across an entire hierarchy; use class instance variables when subclasses need independent state.
  • SomeClass.ancestors and SomeClass.superclass are the best tools for making method lookup concrete when behavior is confusing.