Duck Typing

Published by Programming Line in Ruby. Last updated .

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 implementations
  • caller_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 Comparable and Enumerable do 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:

  1. Ruby evaluates Duck.new, which allocates a new Duck object and runs initialize (inherited from Object, doing nothing extra here).
  2. That object is passed into announce and bound to the local variable speaker.
  3. Inside announce, Ruby evaluates speaker.speak. This is a message send: Ruby takes whatever object speaker currently refers to and searches its method lookup chain — singleton class, then Duck, then any modules Duck includes, then Object, Kernel, BasicObject — for a method literally named speak.
  4. It finds speak defined directly on Duck and calls it, receiving back the string "Quack!" as the method’s return value (the last evaluated expression in the method body, with no explicit return needed).
  5. puts prints 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? over is_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 (<=> or each) and get the rest for free.
  • Let missing methods raise NoMethodError naturally 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 nil and false are 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

  1. Define two classes, Circle (holding a radius) and Square (holding a side), each implementing an area method. Write a total_area(shapes) method that sums the area of 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.
  2. Add a third class to the previous exercise that forgets to implement area. Before running anything, predict exactly what error total_area will raise when it reaches that object, and why.
  3. Extend log_and_cleanup from Example 3 so it also calls flush on the logger, but only when the logger responds to it. Add a BufferedLogger class with a flush method and confirm both FileLogger and ConsoleLogger from 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 checking is_a?/kind_of?.
  • Overusing is_a?/kind_of? checks defeats duck typing and makes code brittle against new classes.
  • Built-in modules like Comparable and Enumerable are 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.