Overview
Ruby implements single inheritance where each class inherits from exactly one parent class, forming a hierarchy that terminates at BasicObject. The super
keyword provides method resolution control, allowing subclasses to invoke parent implementations while extending or modifying behavior.
Every Ruby class inherits from Object by default, which inherits from BasicObject. When defining custom classes, inheritance creates an "is-a" relationship where subclasses gain access to parent methods, constants, and instance variables. The inheritance chain determines method lookup order through Ruby's method resolution algorithm.
class Animal
def initialize(name)
@name = name
end
def speak
"#{@name} makes a sound"
end
end
class Dog < Animal
def speak
super + " - Woof!"
end
end
dog = Dog.new("Rex")
dog.speak
# => "Rex makes a sound - Woof!"
The super
keyword comes in three forms: super
with parentheses passes specific arguments, super
without parentheses passes all current method arguments, and super()
with empty parentheses passes no arguments. Ruby searches the inheritance hierarchy upward until finding a matching method name.
class Vehicle
def start(fuel_type = "gasoline")
"Starting with #{fuel_type}"
end
end
class ElectricCar < Vehicle
def start(battery_level = 100)
# super passes battery_level as fuel_type
super + " at #{battery_level}% battery"
end
def explicit_start
# super() passes no arguments, uses default
super() + " - Electric mode"
end
end
Ruby's inheritance system supports method overriding, where subclasses replace parent implementations entirely, or method extension using super
to build upon parent behavior. The inheritance chain remains accessible even when methods are overridden multiple levels deep.
Basic Usage
Class inheritance uses the <
operator to establish parent-child relationships. Subclasses inherit all instance methods, class methods, constants, and have access to parent instance variables through inherited methods.
class Shape
attr_reader :color
def initialize(color)
@color = color
end
def area
raise NotImplementedError, "Subclass must implement area"
end
def description
"A #{color} shape"
end
end
class Rectangle < Shape
def initialize(color, width, height)
super(color) # calls Shape#initialize with color
@width = width
@height = height
end
def area
@width * @height
end
def description
super + " with area #{area}"
end
end
rect = Rectangle.new("red", 5, 3)
rect.description
# => "A red shape with area 15"
The super
keyword searches the inheritance hierarchy for the next method with the same name. When called without arguments, super
automatically passes all arguments from the current method. This behavior often creates the desired result but requires careful consideration of parameter compatibility.
class Logger
def log(level, message, timestamp = Time.now)
puts "[#{timestamp}] #{level.upcase}: #{message}"
end
end
class FileLogger < Logger
def initialize(filename)
@file = File.open(filename, 'a')
end
def log(level, message, timestamp = Time.now)
# super automatically passes level, message, timestamp
super
@file.puts("[#{timestamp}] #{level.upcase}: #{message}")
@file.flush
end
end
Method resolution follows the inheritance chain upward. Ruby searches the current class first, then the parent class, continuing until finding a matching method or reaching BasicObject. This process applies to both instance and class methods.
class A
def self.class_method
"A's class method"
end
def instance_method
"A's instance method"
end
end
class B < A
def self.class_method
super + " extended by B"
end
def instance_method
super + " extended by B"
end
end
class C < B
def instance_method
super + " and C"
end
end
C.class_method
# => "A's class method extended by B"
C.new.instance_method
# => "A's instance method extended by B and C"
Ruby supports calling super
from class methods, following the same inheritance rules. Class method inheritance operates independently from instance method inheritance, creating separate resolution chains for class-level and instance-level behavior.
Advanced Usage
Multiple inheritance simulation uses modules and prepend
to alter method resolution order. When modules are included, Ruby inserts them into the inheritance chain above the including class. The prepend
method places modules before the class in the lookup order, affecting super
behavior.
module Auditable
def save
puts "Auditing save operation"
super # calls the next save in the chain
puts "Save operation completed"
end
end
module Timestampable
def save
@updated_at = Time.now
super
end
end
class Document
prepend Auditable
include Timestampable
def save
puts "Saving document to database"
@saved = true
end
end
# Method resolution order: Auditable → Document → Timestampable → Object
Document.new.save
# => Auditing save operation
# => Saving document to database
# => Save operation completed
The method
and super_method
methods provide introspection capabilities for inheritance chains. These methods return Method objects that reveal the actual implementation being called and enable dynamic method invocation.
class Parent
def compute(x)
x * 2
end
end
class Child < Parent
def compute(x)
result = super(x)
result + 10
end
def analyze_methods
current = method(:compute)
parent = current.super_method
puts "Current implementation: #{current.source_location}"
puts "Parent implementation: #{parent.source_location}"
# Call parent method directly
parent_result = parent.call(5) # => 10
current_result = current.call(5) # => 20
end
end
Dynamic super
invocation handles cases where method arguments change between parent and child implementations. Argument transformation often requires explicit parameter handling rather than relying on automatic argument passing.
class APIClient
def request(endpoint, method: :get, headers: {}, **options)
puts "Making #{method} request to #{endpoint}"
puts "Headers: #{headers}"
puts "Options: #{options}"
end
end
class AuthenticatedClient < APIClient
def initialize(api_key)
@api_key = api_key
end
def request(endpoint, auth: true, **options)
if auth
headers = options.fetch(:headers, {})
headers['Authorization'] = "Bearer #{@api_key}"
options[:headers] = headers
end
# Transform arguments for parent method
super(endpoint, **options)
end
end
client = AuthenticatedClient.new("secret123")
client.request("/users", method: :post, body: {name: "John"})
# => Making post request to /users
# => Headers: {"Authorization"=>"Bearer secret123"}
# => Options: {:method=>:post, :body=>{:name=>"John"}}
Inheritance with singleton methods and eigenclasses creates complex method resolution scenarios. When classes inherit from parents with singleton methods, the inheritance affects both instance and class-level behavior differently.
class Database
class << self
def connection
@connection ||= establish_connection
end
private
def establish_connection
"Connected to primary database"
end
end
end
class ReadOnlyDatabase < Database
class << self
def connection
@connection ||= establish_readonly_connection
end
private
def establish_readonly_connection
"Connected to readonly database"
end
# Can call parent's private class method
def establish_connection
super + " (inherited)"
end
end
end
Common Pitfalls
The super
keyword without parentheses passes all current method arguments, which creates problems when parent and child method signatures differ. This automatic argument passing often leads to ArgumentError exceptions when parent methods expect different parameters.
class Parent
def process(data)
"Processing: #{data}"
end
end
class Child < Parent
def process(data, options = {})
# WRONG: super passes both data and options
# Parent method only accepts one argument
begin
super # ArgumentError: wrong number of arguments
rescue ArgumentError => e
puts "Error: #{e.message}"
end
# CORRECT: explicitly pass only compatible arguments
super(data) + " with options: #{options}"
end
end
Child.new.process("test", format: :json)
Method resolution bypasses private and protected visibility, but calling super
from methods with different visibility can expose methods unintentionally. Ruby's method visibility operates independently from the inheritance chain, creating security implications.
class SecureClass
private
def sensitive_operation
"Performing sensitive operation"
end
protected
def internal_method
sensitive_operation
end
end
class PublicClass < SecureClass
def public_method
# This exposes the protected internal_method publicly
internal_method
end
def internal_method
# super calls protected parent method
"Public: " + super
end
end
obj = PublicClass.new
obj.public_method # Unintentionally exposes protected method
Module inclusion and prepending affects super
behavior in counterintuitive ways. The method resolution order changes based on how modules are mixed in, causing super
calls to skip expected methods or invoke unexpected implementations.
module A
def test
"A: " + super
end
end
module B
def test
"B: " + super
end
end
class Base
def test
"Base"
end
end
class IncludeOrder < Base
include A
include B # B comes after A in resolution
end
class PrependOrder < Base
prepend A
prepend B # B comes before A in resolution
end
IncludeOrder.new.test
# => "B: A: Base" (B → A → Base)
PrependOrder.new.test
# => "A: B: Base" (A → B → PrependOrder → Base)
Block passing with super
requires explicit handling because blocks are not automatically forwarded. When parent methods expect blocks but child methods don't explicitly handle them, the block gets lost in the inheritance chain.
class Iterator
def each_item(items)
items.each { |item| yield(item) if block_given? }
end
end
class FilteredIterator < Iterator
def each_item(items, filter: nil)
filtered = filter ? items.select(&filter) : items
# WRONG: block is not passed to super
super(filtered)
# CORRECT: explicitly pass the block
# super(filtered, &block)
end
end
# This won't work as expected
FilteredIterator.new.each_item([1, 2, 3, 4]) { |n| puts n * 2 }
Infinite recursion occurs when super
calls create circular dependencies in the inheritance chain. This commonly happens with module inclusion where multiple modules define the same method and call super
without terminating conditions.
module Middleware1
def process
puts "Middleware1 before"
super
puts "Middleware1 after"
end
end
module Middleware2
def process
puts "Middleware2 before"
super # This will cause infinite recursion if no base method exists
puts "Middleware2 after"
end
end
class Processor
include Middleware1
include Middleware2
# Missing base implementation causes infinite recursion
# def process
# puts "Base processing"
# end
end
# Processor.new.process # SystemStackError: stack level too deep
Reference
Inheritance Syntax
Syntax | Description | Example |
---|---|---|
class Child < Parent |
Defines inheritance relationship | class Dog < Animal |
Class.superclass |
Returns parent class | String.superclass # => Object |
obj.class.ancestors |
Shows inheritance chain | "".class.ancestors |
Class < ParentClass |
Tests inheritance relationship | String < Object # => true |
Super Keyword Forms
Form | Arguments Passed | Description |
---|---|---|
super |
All current method arguments | Automatic argument forwarding |
super() |
No arguments | Explicit no arguments |
super(arg1, arg2) |
Specified arguments only | Explicit argument control |
super(&block) |
All arguments plus explicit block | Block forwarding |
Method Resolution Methods
Method | Returns | Description |
---|---|---|
method(:name) |
Method object |
Current method implementation |
method(:name).super_method |
Method object or nil |
Parent method implementation |
method(:name).owner |
Class or Module |
Where method is defined |
method(:name).source_location |
Array or nil | File and line number |
respond_to?(:method_name) |
Boolean | Whether object responds to method |
Class Hierarchy Methods
Method | Returns | Description |
---|---|---|
Class.ancestors |
Array of modules/classes | Full inheritance chain |
Class.superclass |
Class or nil | Immediate parent class |
Module.nesting |
Array | Current nesting context |
Class.included_modules |
Array | Included modules only |
obj.singleton_class |
Class | Object's eigenclass |
Visibility and Inheritance
Visibility | Inherited | Callable via super | Description |
---|---|---|---|
public |
Yes | Yes | Accessible everywhere |
protected |
Yes | Yes | Accessible within class hierarchy |
private |
Yes | Yes | Accessible only within defining class |
Module Inclusion Effects
Method | Position in Chain | Super Behavior |
---|---|---|
include ModuleName |
After class | Calls continue up chain |
prepend ModuleName |
Before class | Module methods wrap class methods |
extend ModuleName |
Singleton class | Adds methods to eigenclass |
Common Error Types
Error | Cause | Solution |
---|---|---|
ArgumentError |
Wrong argument count in super | Use explicit arguments |
NoMethodError |
Method not found in chain | Check inheritance hierarchy |
SystemStackError |
Infinite recursion | Add base case or check module chain |
NameError |
Undefined constant | Check constant lookup rules |
Inheritance Chain Examples
# Basic inheritance chain
Object.ancestors
# => [Object, Kernel, BasicObject]
# With modules
module M; end
class C; include M; end
C.ancestors
# => [C, M, Object, Kernel, BasicObject]
# With prepend
class D; prepend M; end
D.ancestors
# => [M, D, Object, Kernel, BasicObject]
Method Resolution Order Rules
- Singleton methods (eigenclass)
- Modules prepended to eigenclass
- Eigenclass ancestors
- Modules prepended to class
- Class methods
- Modules included in class
- Superclass chain (repeat from step 4)