Duck Typing: Ruby's Quiet Superpower
There’s a phrase that gets thrown around in Ruby circles: “If it walks like a duck and quacks like a duck, it’s a duck.” It sounds whimsical, but it’s actually one of Ruby’s most powerful design principles.
What Duck Typing Actually Means
In statically typed languages, you care about what something is. In Ruby, you care about what something can do.
def process(item)
item.to_s
end
This method doesn’t care if item is a String, Integer, Array, or a custom object you wrote last Tuesday. It only cares that item responds to to_s. That’s duck typing in action.
The Freedom This Creates
Consider a logging system:
class Logger
def initialize(output)
@output = output
end
def log(message)
@output.puts("[#{Time.now}] #{message}")
end
end
This logger works with $stdout, File.open('log.txt', 'a'), StringIO.new, or any object that responds to puts. You didn’t need to define an interface. You didn’t need inheritance. You just needed an object that does the right thing.
Duck Typing Makes Testing Trivial
This is where duck typing really shines. Need to test that logger without writing to a real file?
class FakeOutput
attr_reader :messages
def initialize
@messages = []
end
def puts(message)
@messages << message
end
end
def test_logger_formats_messages
output = FakeOutput.new
logger = Logger.new(output)
logger.log("hello")
assert_match(/\[.*\] hello/, output.messages.first)
end
No mocking framework required. No complex dependency injection setup. Just an object that quacks correctly.
And when your tests fail? Duck typing means you’re often comparing what an object produced versus what you expected, frequently hashes or structured data. That’s where tools like RubyHash come in handy, letting you paste two hashes and instantly see the diff.
The Trade-off
Duck typing isn’t free. You lose compile-time guarantees. If you pass an object that doesn’t respond to the expected method, you’ll find out at runtime, hopefully in your tests, not production.
But Ruby gives you tools to handle this gracefully:
def process(item)
if item.respond_to?(:to_s)
item.to_s
else
raise ArgumentError, "item must respond to #to_s"
end
end
Or more idiomatically, you trust your tests and let it fail fast with a clear NoMethodError.
Composition Over Inheritance
Duck typing naturally pushes you toward composition. Instead of building elaborate class hierarchies, you build small objects that do one thing well and combine them freely.
class Encryptor
def process(data)
# encrypt the data
end
end
class Compressor
def process(data)
# compress the data
end
end
class Pipeline
def initialize(*processors)
@processors = processors
end
def run(data)
@processors.reduce(data) { |d, p| p.process(d) }
end
end
pipeline = Pipeline.new(Compressor.new, Encryptor.new)
pipeline.run("sensitive data")
The Pipeline doesn’t know or care what those processors are. It just needs them to respond to process. Add a new processor? Just write a class with a process method. Done.
When Types Help
Ruby 3 introduced RBS and type checking tools like Sorbet exist for teams that want more safety. These are valuable for large codebases where the flexibility of duck typing can become a liability.
But even with types, the duck typing mindset remains useful. You’re still thinking about behavior, not taxonomy. You’re still asking “what can this do?” rather than “what is this?”
The Duck Typing Mindset
Duck typing is more than a language feature, it’s a way of thinking about software. It encourages:
- Small, focused interfaces: Objects only need to implement what’s actually used
- Flexibility: Swap implementations without changing calling code
- Testability: Create simple test doubles that quack correctly
- Composition: Build systems from interchangeable parts
The next time you’re tempted to check is_a? or build a class hierarchy, pause. Ask yourself: do I actually care what this is, or just what it can do?
Usually, it’s the latter. And that’s when you let the duck quack.
Debugging test failures with complex hashes? Try RubyHash, paste your expected and actual values for a clean, readable diff.
Enjoyed this post?
Subscribe to get notified when we publish more Ruby and Rails content.