← Back to blog

Duck Typing: Ruby's Quiet Superpower

· Lachlan Young

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:

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.