Duck Typing
Duck typing is Ruby’s approach to polymorphism: instead of asking "what class is this object?", you ask "does this object respond to the method I need?" The name comes from the old saying — if it walks like a duck and quacks like a duck, treat it as a duck, regardless of what it actually is underneath. In Ruby, this isn’t just a nice idea; it’s baked into how the language dispatches every single method call, and understanding it is the key to writing idiomatic, flexible Ruby code.
Overview: How Duck Typing Works
Ruby is dynamically typed, which means the interpreter doesn’t check an object’s type until the moment a method is actually called on it, at runtime. There is no compiler cross-checking that a variable declared as some interface actually implements the methods you’re about to invoke. Instead, when you write obj.speak, Ruby simply looks for a method named speak on whatever object obj happens to be at that moment and calls it if one exists. Ruby does not care whether obj is a Duck, a Robot, or a class you haven’t even written yet — as long as the object understands the message speak, the call succeeds.
This is fundamentally different from typical statically typed polymorphism, where a value usually has to implement a declared interface or inherit from a common superclass before it’s considered interchangeable with its siblings. Ruby has no formal interface keyword at all. Instead, an "interface" in Ruby is just an informal agreement: any object that responds to a given set of method names can be used wherever that behavior is expected. That informal, behavior-based contract is what people mean by duck typing.
Under the hood, every method call in Ruby is really a message being sent to an object — this is a direct inheritance from Smalltalk-style object systems, which strongly influenced Ruby’s design. When Ruby evaluates obj.speak, it walks the object’s method lookup chain: first its singleton class (methods defined on that one specific object), then its class, then any modules mixed in with include (searched in reverse order of inclusion), then the superclass and its included modules, and so on up to Object, Kernel, and finally BasicObject. The first matching speak method found anywhere along that chain wins and is executed. If nothing in the entire chain defines speak, Ruby invokes method_missing, which by default raises NoMethodError. Duck typing works precisely because this lookup happens by method name at call time, not by declared type at compile time — two completely unrelated classes can each define their own speak method and both satisfy the exact same calling code.
The tool that complements this is respond_to?, which asks an object "do you have a method with this name?" without actually calling it. This lets you write code that adapts to an object’s capabilities rather than to its ancestry.
Duck Typing vs. Explicit Type Checks
| Approach | Example | What it means |
|---|---|---|
obj.speak |
Just call the method | True duck typing — trust the object to respond correctly |
obj.is_a?(Duck) |
Class check | Static-style thinking — brittle, defeats duck typing |
obj.respond_to?(:speak) |
Capability check | Still duck typing — checks behavior, not class |
Reaching for is_a? or kind_of? to branch on an object’s class is usually a sign you’re fighting Ruby’s grain — it reintroduces exactly the kind of rigid coupling duck typing exists to avoid.
Syntax
There is no special syntax for duck typing — it emerges naturally any time two or more unrelated classes implement a method with the same name, and calling code simply invokes that method without first checking the object’s class. The general shape looks like this:
class A
def method_name(*args)
# A's own implementation
end
end
class B
def method_name(*args)
# B's own implementation, unrelated to A
end
end
def caller_method(obj, *args)
obj.method_name(*args) # works for any object that responds to method_name
end
method_name— the shared method name each participating class implements independently, with no common ancestor required*args— whatever arguments the shared method needs; the signature should stay consistent across implementationscaller_method— ordinary code that just sends the message and trusts the object to handle it correctly- No shared superclass or module is required, though you can mix in one purely to document the expected protocol (as
ComparableandEnumerabledo in Ruby’s own standard library)
Examples
Example 1: Unrelated Classes Sharing a Method Name
Three classes with nothing in common — not even a shared superclass beyond Object — each implement speak. The calling code never checks which class it received.
class Duck
def speak
"Quack!"
end
end
class Dog
def speak
"Woof!"
end
end
class Robot
def speak
"BEEP BOOP"
end
end
def announce(speaker)
puts speaker.speak
end
[Duck.new, Dog.new, Robot.new].each { |animal| announce(animal) }
Output:
Quack!
Woof!
BEEP BOOP
announce never asks what class its argument is. It just sends the speak message and prints whatever comes back. Because Duck, Dog, and Robot each define their own speak, all three work identically from announce‘s point of view — that’s duck typing giving you polymorphism for free.
Example 2: A More Realistic Case — a Shopping Cart
Here, Book and DigitalDownload share no inheritance relationship beyond Object, but both expose name and price, so a single print_receipt method can handle either.
class Book
attr_reader :name, :price
def initialize(name:, price:)
@name = name
@price = price
end
end
class DigitalDownload
attr_reader :name, :price
def initialize(name:, price:)
@name = name
@price = price
end
end
def print_receipt(items)
total = 0
items.each do |item|
puts "#{item.name}: $#{'%.2f' % item.price}"
total += item.price
end
puts "Total: $#{'%.2f' % total}"
end
cart = [
Book.new(name: "The Pragmatic Programmer", price: 39.95),
DigitalDownload.new(name: "Ruby Icon Pack", price: 4.99)
]
print_receipt(cart)
Output:
The Pragmatic Programmer: $39.95
Ruby Icon Pack: $4.99
Total: $44.94
print_receipt was written once and works for both product types, and it would keep working for a future SubscriptionPlan class tomorrow, with zero changes, as long as that class also exposes name and price.
Example 3: Optional Behavior With respond_to?
Sometimes only some of the objects you’ll receive support an extra piece of behavior. respond_to? lets you check for that capability instead of assuming every object has it.
class FileLogger
def write(message)
puts "LOG: #{message}"
end
def close
puts "FileLogger closed"
end
end
class ConsoleLogger
def write(message)
puts "CONSOLE: #{message}"
end
end
def log_and_cleanup(logger, message)
logger.write(message)
logger.close if logger.respond_to?(:close)
end
log_and_cleanup(FileLogger.new, "starting job")
log_and_cleanup(ConsoleLogger.new, "starting job")
Output:
LOG: starting job
FileLogger closed
CONSOLE: starting job
ConsoleLogger never defines close, and that’s fine: respond_to?(:close) returns false for it, so the call is simply skipped instead of raising an error. This is still duck typing — the check is about behavior ("can you close?"), never about class.
How It Works Step by Step
Walk through exactly what happens when announce(Duck.new) runs in Example 1:
- Ruby evaluates
Duck.new, which allocates a newDuckobject and runsinitialize(inherited fromObject, doing nothing extra here). - That object is passed into
announceand bound to the local variablespeaker. - Inside
announce, Ruby evaluatesspeaker.speak. This is a message send: Ruby takes whatever objectspeakercurrently refers to and searches its method lookup chain — singleton class, thenDuck, then any modulesDuckincludes, thenObject,Kernel,BasicObject— for a method literally namedspeak. - It finds
speakdefined directly onDuckand calls it, receiving back the string"Quack!"as the method’s return value (the last evaluated expression in the method body, with no explicitreturnneeded). putsprints that returned string followed by a newline.
Nothing in this sequence ever asks "is this object allowed to be used here?" the way a statically typed language would check an interface. The only requirement was that whatever object showed up at step 3 had a speak method somewhere in its lookup chain. Swap in a Dog or a Robot and the exact same five steps play out, just with a different method body executing at step 4 — that’s polymorphism achieved purely through shared method names.
Common Mistakes
Mistake 1: Checking Class Instead of Trusting Behavior
def make_it_speak(animal)
if animal.is_a?(Duck)
puts animal.speak
elsif animal.is_a?(Dog)
puts animal.speak
else
puts "I don't know how to make this speak"
end
end
This only works for the exact classes the author thought of in advance. Add a Robot class next month, and make_it_speak silently falls into the else branch even though Robot implements speak perfectly well. Every caller now has to be updated whenever a new type appears — the opposite of what polymorphism is supposed to buy you. The fix is to simply trust the object:
def make_it_speak(animal)
puts animal.speak
end
This version works today for Duck, Dog, and Robot, and it will keep working for any future class that implements speak, with no changes required here at all.
Mistake 2: Assuming Every Object Supports the Method You Need
class FreeSample
attr_reader :name
def initialize(name:)
@name = name
end
end
def total_price(cart)
cart.sum(&:price)
end
samples = [FreeSample.new(name: "Trial Pack")]
puts total_price(samples)
Here, Array#sum calls .price on every element, but FreeSample never defined a price method. Ruby walks its entire lookup chain, finds nothing named price, and falls through to method_missing, which raises NoMethodError: undefined method 'price' for an instance of FreeSample. Duck typing means trusting objects that implement the right interface — it does not mean skipping the check when an object might not. Guard the optional case instead:
def total_price(cart)
cart.sum { |item| item.respond_to?(:price) ? item.price : 0 }
end
Now an item without a price method simply contributes 0 to the total instead of crashing the whole calculation.
Best Practices
- Prefer
respond_to?overis_a?/kind_of?when your code genuinely needs to branch on capability — it keeps the door open for future classes you haven’t written yet. - Document the expected "protocol" (which method names and arities your code relies on) in a comment or test, since Ruby has no formal interface declaration to read.
- Where a shared protocol is genuinely conceptual — comparable objects, enumerable collections — mix in the matching core module (
Comparable,Enumerable) instead of reinventing it; you implement one primitive method (<=>oreach) and get the rest for free. - Let missing methods raise
NoMethodErrornaturally rather than defensively rescuing it everywhere; a clear error and stack trace is usually more useful than a silently swallowed failure. - Write tests against the behavior you depend on, not the class of the object — a suite that exercises every implementation through the same shared method calls doubles as documentation of your duck-typed interface.
- Remember only
nilandfalseare falsy in Ruby; don’t conflate "object is falsy" with "object doesn’t respond to the method" when debugging duck-typed code — they’re unrelated checks.
Practice Exercises
- Define two classes,
Circle(holding aradius) andSquare(holding aside), each implementing anareamethod. Write atotal_area(shapes)method that sums theareaof every shape in an array without ever checking the shapes’ classes. Test it with one of each and confirm the total matches your own hand calculation. - Add a third class to the previous exercise that forgets to implement
area. Before running anything, predict exactly what errortotal_areawill raise when it reaches that object, and why. - Extend
log_and_cleanupfrom Example 3 so it also callsflushon the logger, but only when the logger responds to it. Add aBufferedLoggerclass with aflushmethod and confirm bothFileLoggerandConsoleLoggerfrom the lesson still behave correctly.
Summary
- Duck typing means Ruby cares whether an object responds to a method, not what class it belongs to.
- Method calls in Ruby are resolved at runtime via message passing and the ancestor chain, not compile-time type checking — that’s what makes duck typing possible.
respond_to?lets you check an object’s capabilities without invoking a method or checkingis_a?/kind_of?.- Overusing
is_a?/kind_of?checks defeats duck typing and makes code brittle against new classes. - Built-in modules like
ComparableandEnumerableare Ruby’s own examples of protocol-based, duck-typed design. - Trust objects that implement the right methods, but guard genuinely optional behavior with
respond_to?rather than assuming every object supports it.
