Duck typing, and how Python handles types
What duck typing is, how ABCs and Protocol affect it, and how type hints coexist with duck typing.
“If it walks like a duck and quacks like a duck, it’s a duck.”
What is duck typing?
Duck typing is the programming philosophy of judging an object’s type by the methods and attributes it has, not by its class declaration. It is a common approach in dynamically typed languages such as Python, JavaScript, and Ruby.
Rather than checking the type explicitly, you simply call the behaviour you need. If the method or attribute exists, it works; if not, you get a runtime error.
class Duck:
def sound(self):
return "Quack!"
class Dog:
def sound(self):
return "Woof!"
class Robot:
def sound(self):
return "Beep!"
def make_sound(animal):
# just call it, no type check
print(animal.sound())
make_sound(Duck()) # Quack!
make_sound(Dog()) # Woof!
make_sound(Robot()) # Beep!
Duck, Dog, Robot — all are treated identically as long as they have a sound() method. The focus is on “what can this object do?” rather than “what is this object?”
Do ABCs break duck typing?
The short answer is not necessarily. It depends on how you use them.
Forcing an isinstance() check → duck typing broken
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def sound(self): pass
def make_sound(animal):
if not isinstance(animal, Animal): # checking the type directly!
raise TypeError("not an Animal")
print(animal.sound())
make_sound(Robot()) # ❌ rejected even though it has sound()
ABC as specification only, no check → duck typing preserved
If you do not check the type inside the function, duck typing keeps working even with an ABC in play. All the ABC does then is act as an agreement among developers: “this method must be implemented.”
A compromise with register()
Animal.register(Robot) # register Robot as an Animal
print(isinstance(Robot(), Animal)) # ✅ True (passes without inheritance)
This is a middle ground that keeps duck typing’s flexibility while making partial use of the type system. The drawback is that a developer has to register things manually.
| Situation | Duck typing |
|---|---|
ABC + forced isinstance() check | ❌ broken |
| ABC defined only, called without a check | ✅ preserved |
ABC + register() | 〰 compromise |
Type hints have nothing to do with duck typing
Type hints are completely ignored at runtime. Writing animal: Duck does not make the Python interpreter check anything during execution.
def make_sound(animal: Duck) -> str:
return animal.sound()
make_sound(Robot()) # ✅ runs without any problem
What type hints actually do is this:
- Static analysis: type checkers like mypy and pyright warn you before the code runs
- IDE support: used for autocompletion and warnings
- Documentation: conveying intent to whoever reads the code
Wiring mypy into CI effectively constrains duck typing, but that is a check at development time. The runtime behaviour is still duck typing.
Protocol: duck typing for type hints
When you want to use mypy and still keep the spirit of duck typing, you use Protocol. The whole reason Protocol exists is type hints and static analysis.
from typing import Protocol
class Soundable(Protocol):
def sound(self) -> str: ...
def make_sound(animal: Soundable) -> str:
# "having a sound() method is enough"
return animal.sound()
class Robot: # without inheriting from Soundable
def sound(self):
return "Beep!"
make_sound(Robot()) # ✅ passes mypy, passes at runtime
This is called structural subtyping. It treats an object as belonging to a type if it has the required methods, with no inheritance relationship — the spirit of duck typing, implemented inside the world of type hints.
@runtime_checkable: using it at runtime too
from typing import Protocol, runtime_checkable
@runtime_checkable
class Soundable(Protocol):
def sound(self) -> str: ...
print(isinstance(Robot(), Soundable)) # ✅ True
# checks for the presence of the methods automatically
This resembles an ABC’s register(), but there is no manual registration: the decision is made automatically from the presence of the methods.
| Approach | Duck typing | Static analysis | Runtime isinstance |
|---|---|---|---|
| Pure duck typing | ✅ | ❌ | ❌ |
ABC + register() | 〰 | ❌ | ✅ (manual) |
| Protocol | ✅ | ✅ | ❌ |
Protocol + runtime_checkable | ✅ | ✅ | ✅ (automatic) |
Summary
- Duck typing judges a type by “what it can do”, not “what it is”.
- Depending on how you use them, ABCs can either break duck typing or preserve it.
- Type hints are ignored at runtime, so they do not directly affect duck typing.
- Protocol is the way to keep the duck typing philosophy in a type-hinted, statically analysed codebase.