super
When a subclass overrides a method, it often needs to build on what the parent class already does rather than replace it entirely. Ruby’s super keyword lets a method call the next matching version of itself further up the ancestor chain, so a subclass can add behavior without duplicating the parent’s logic. It shows up constantly in initialize methods, in modules mixed into a class, and anywhere you want composition instead of copy-paste.
Overview: How super Works
Every object in Ruby resolves a method call by walking its ancestor chain: first the object’s singleton class (if it has singleton methods), then its class, then any modules mixed into that class via include (searched in reverse order of inclusion), then the superclass and its own included modules, and so on up through Object, Kernel, and BasicObject. You can see this chain yourself by calling SomeClass.ancestors. When you define a method in a subclass with the same name as one further up the chain, you are not deleting the original method — it still exists, just shadowed. super is how you reach it.
Calling super inside a method resumes the search for that same method name starting right after the point in the ancestor chain where the current method was found, and calls whatever it finds there. This is exactly the mechanism that makes mixins and multi-level inheritance work together: a module included between a class and its superclass can itself call super and hand control further up the chain, forming a chain of cooperating methods rather than one method simply replacing another.
super comes in three forms that behave differently:
- Bare
super(no parentheses) automatically forwards the exact arguments, keyword arguments, and block that the current method received — not necessarily their original values, since if you reassign a parameter before callingsuper, the new value is what gets forwarded. super()with empty parentheses calls the next method with no arguments at all, even if the current method received some.super(args)calls the next method with exactly the arguments you list, which may differ from what the current method received.
One more detail worth knowing: include inserts a module below the including class in the ancestor chain (the class itself is searched first, then the module), while prepend inserts a module above the class (the module is searched first). A prepended module’s method runs before the class’s own method, and calling super from inside it reaches the class’s method — effectively wrapping it. This is less common than plain include, but it explains why what super reaches depends on how a module was mixed in, not just where the method text happens to be written.
Syntax
def method_name(args)
super # forwards this method's own arguments and block automatically
super() # calls the next method with no arguments
super(a, b) # calls the next method with exactly these arguments
end
| Form | Arguments passed | When to use it |
|---|---|---|
super |
Same args, keyword args, and block the current method received | The common case — you want the parent to do what it always does with the same input |
super() |
None | You want to call the parent version but deliberately not pass along any arguments |
super(args) |
Exactly what you list | You want to transform or filter the arguments before the parent sees them |
Examples
Example 1: Bare super in a simple override
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")
puts dog.speak
Output:
Rex makes a sound. Woof!
Dog doesn't override initialize, so Dog.new("Rex") runs Animal#initialize directly and sets @name. Inside Dog#speak, the bare super call takes no arguments — because speak itself received none — and calls Animal#speak, which returns "Rex makes a sound.". That string is interpolated into Dog#speak's own return value, producing "Rex makes a sound. Woof!".
Example 2: super with explicit arguments and super()
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
def describe
"#{@make} #{@model}"
end
end
class Car < Vehicle
def initialize(make, model, doors:)
super(make, model)
@doors = doors
end
def describe
"#{super()} with #{@doors} doors"
end
end
car = Car.new("Toyota", "Corolla", doors: 4)
puts car.describe
Output:
Toyota Corolla with 4 doors
Car#initialize takes a keyword argument doors: that Vehicle#initialize knows nothing about, so it can't use bare super — it explicitly calls super(make, model) to forward only the two positional arguments Vehicle#initialize expects. In describe, super() with empty parentheses calls Vehicle#describe with no arguments (which is correct, since Vehicle#describe doesn't take any), and the result is woven into the subclass's own string.
Example 3: super traveling through a mixed-in module
module Loggable
def describe
"[LOG] #{super}"
end
end
class Vehicle
def describe
"#{@make} #{@model}"
end
end
class Car < Vehicle
include Loggable
def initialize(make, model)
@make = make
@model = model
end
def describe
"#{super} (a car)"
end
end
puts Car.ancestors.first(4).inspect
puts Car.new("Honda", "Civic").describe
Output:
[Car, Loggable, Vehicle, Object]
[LOG] Honda Civic (a car)
Because Loggable is included in Car, it sits between Car and Vehicle in the ancestor chain, as the printed ancestors list confirms. When Car#describe calls super, Ruby doesn't jump straight to Vehicle — it finds Loggable#describe first, which itself calls super to reach Vehicle#describe. Three cooperating methods contribute to one final string.
How It Works Step by Step
Walking through Example 3 shows exactly how super travels through both a mixed-in module and a superclass:
- Ruby calls
describeon aCarinstance. Method lookup starts atCaritself, the front of the ancestor chain, and findsCar#describeimmediately. Car#describeruns and hitssuper. Ruby resumes the search fordescribestarting right afterCarin the ancestor chain — that'sLoggable, becauseinclude Loggableinserted it betweenCarandVehicle.Loggable#describeruns and itself callssuper. Ruby resumes the search afterLoggable, which lands onVehicle.Vehicle#describeruns, returns"Honda Civic", and has no furthersupercall, so the chain starts unwinding from here.- Back in
Loggable#describe, the returned string is wrapped into"[LOG] Honda Civic". - Back in
Car#describe, that result is wrapped again into"[LOG] Honda Civic (a car)", which is what actually gets printed.
Common Mistakes
Mistake 1: Bare super forwards ALL current arguments, even ones the parent can't accept
It's easy to assume bare super is always safe. It isn't — it forwards every argument the current method received, so if a subclass method takes more parameters than the version it's overriding, bare super raises an ArgumentError.
class Shape
def initialize(color)
@color = color
end
end
class Square < Shape
def initialize(color, side)
super # BUG: bare super forwards BOTH color and side, but Shape#initialize takes only 1 argument
@side = side
end
end
begin
Square.new("red", 4)
rescue ArgumentError => e
puts "Error: #{e.message}"
end
Output:
Error: wrong number of arguments (given 2, expected 1)
The fix is to switch to explicit super(color), sending only the argument Shape#initialize actually expects:
class Shape
def initialize(color)
@color = color
end
end
class Square < Shape
def initialize(color, side)
super(color) # FIX: pass only the argument Shape#initialize expects
@side = side
end
end
square = Square.new("red", 4)
puts "Side: #{square.instance_variable_get(:@side)}"
Output:
Side: 4
Mistake 2: Calling super when nothing further up the chain defines that method
super only works if some ancestor actually defines a method with the same name. Calling it from a method that exists only in the current class raises NoMethodError.
class Base
def greet
"Hello from Base"
end
end
class Sub < Base
def farewell
"Bye! #{super}" # BUG: Base has no `farewell` method for super to call
end
end
begin
Sub.new.farewell
rescue NoMethodError
puts "Error: no superclass method to call"
end
Output:
Error: no superclass method to call
Before adding super to a method, check that a class or module further up ancestors actually defines that same method name.
Mistake 3: Forgetting to call super in initialize leaves inherited state unset
Unlike some languages, Ruby never calls a parent's initialize for you automatically. If a subclass defines its own initialize and never calls super, the parent's setup code simply never runs, and any instance variables it would have set stay nil.
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
class Employee < Person
def initialize(name, salary)
@salary = salary # BUG: forgot to call super, so @name is never set
end
def info
"#{name} earns #{@salary}"
end
end
emp = Employee.new("Ava", 50_000)
puts emp.info
Output:
earns 50000
name silently returns nil (which interpolates as an empty string) because Person#initialize never ran. Adding super(name) fixes it:
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
class Employee < Person
def initialize(name, salary)
super(name) # FIX: let Person#initialize set @name
@salary = salary
end
def info
"#{name} earns #{@salary}"
end
end
emp = Employee.new("Ava", 50_000)
puts emp.info
Output:
Ava earns 50000
Best Practices
- Prefer bare
superto automatically forward arguments, keyword arguments, and blocks unless you deliberately need to change what's passed on. - Always call
superininitializewhen subclassing a class that sets up its own state, unless you intentionally want to skip that setup. - Use
super()with empty parentheses when you want to call the parent version but deliberately drop the arguments the current method received — never rely on baresuperjust because it happens to work. - Remember that
superfollows the full ancestor chain, including mixed-in modules, not just the literal superclass — checkSomeClass.ancestorswhen the flow isn't obvious. - Call
superearly ininitialize(usually first) so inherited instance variables are set up before the rest of the method touches them. - Don't sprinkle
supercalls defensively wrapped inrescue NoMethodError— if you're not sure the method exists further up the chain, that's a sign to checkancestors, not to silence the error.
Practice Exercises
- Define a class
Bookwithinitialize(title, author)and ato_smethod that returns"#{title} by #{author}". Then defineEBook < Bookthat adds afile_size_mbkeyword argument and overridesto_sto callsuperand append" (#{file_size_mb}MB)". Instantiate anEBookand print it. - Write a module
Timestampedwith asavemethod that prints"Saved at a fixed point in time"and then callssuper. Include it in a classDocumentthat defines its ownsavemethod printing"Document saved". CheckDocument.ancestors, and predict what happens when you callDocument.new.saveifDocument#savedoes not itself callsuper. - Build a three-level hierarchy:
Vehicle,Car < Vehicle, andSportsCar < Car. Give each class adescribemethod that callssuperand prepends its own class name, producing something like"SportsCar -> Car -> Vehicle". Instantiate aSportsCarand calldescribe— predict the exact string before running it.
Summary
supercalls the next matching method up an object's ancestor chain — the same chainSomeClass.ancestorsshows you, which includes mixed-in modules, not just superclasses.- Bare
superforwards the current method's own arguments, keyword arguments, and block automatically;super()forwards nothing;super(args)forwards exactly what you specify. includeinserts a module below the including class in the chain;prependinserts it above, changing whatsuperreaches from inside the class's own method.- Forgetting to call
superin a subclass'sinitializesilently skips the parent's setup — instance variables the parent would have set staynil. - Calling bare
superwith the wrong arity, or callingsuperwhen nothing further up the chain defines that method, raisesArgumentErrororNoMethodErrorat runtime — the syntax itself is always valid either way.
