"If it walks like a duck and quacks like a duck, it's a duck." That single sentence captures Python's whole attitude toward types. The language doesn't care what class an object came from; it cares whether the object can do what the next call requires. This lesson covers how duck typing works, why Python relies on it so heavily, where it works well, where it causes problems, and the tools available to make it safer without giving up the flexibility.
Duck typing is a style of polymorphism where an object's suitability is decided by the methods and attributes it has, not by any declared type or inheritance relationship. If a function calls item.calculate_shipping(), any object with that method works. The function doesn't know the class and doesn't need to ask.
Three unrelated classes. No common parent, no shared base, no implements clause. The function still works because all three classes answer the same call with a number. The "contract" between the function and its inputs lives entirely in the methods being called, not in any type declaration.
This is the opposite of how languages like Java or C# work, where every type has to declare what interfaces it implements before it can be used in a polymorphic context. Python skips that step. The bargain is: classes can be defined however suits the design, as long as they respond to the right calls when the time comes.
The diagram shows the shape every duck-typed call has. The caller asks for a behavior, the loop dispatches to whatever object it has, and the object decides how to answer. The classes in green don't need to know about each other, and the caller doesn't need to know which classes exist.
Duck typing isn't a niche pattern for handwritten code. It's how big chunks of Python's standard library work. The "file-like object" is the classic example: any object with a .read() method that returns text or bytes can be treated as a file by most code that expects one.
count_words doesn't check whether its input is a real file. It calls .read() and trusts the answer. A real file from open() works. A StringIO buffer works. A handwritten class with a read method works. None of them share a parent class with the others, and the function never required it.
The same pattern shows up with iterables. Anything with an __iter__ method (or even a __getitem__ that accepts integer indices starting at zero) can be used in a for loop:
The for loop doesn't care whether cart is a list, a dict, a generator, or a custom class. It calls iter(cart) and expects something back. CartCounter provides __iter__, so the loop succeeds. The standard library is built around these implicit protocols: read(), __iter__, __len__, __getitem__, __call__. Any class can opt into them by defining the right method.
A duck-typed function has to call a method to find out if it works. There's no way to ask "would this work?" without trying. hasattr(obj, "read") checks the name but not the signature, and protocols are the only way to verify the shape ahead of time.
Some languages use nominal typing, where a value's compatibility depends on its declared type name. Java, C#, and Go (with named types) all work this way. A class must declare that it implements the right interface up front; the compiler then checks the relationship by name.
Python uses structural typing with duck typing: compatibility is decided by shape, not by name. The same trade-off shows up in typing.Protocol, which brings structural typing into Python's type hints. The shape decides; the declared name does not.
| Aspect | Nominal Typing (Java, C#) | Duck Typing (Python) |
|---|---|---|
| Compatibility decided by | Declared type name and explicit implements | Methods and attributes the object has |
| When mismatches are caught | Compile time | Runtime, at the moment of the call |
| Adding a new type | Must declare every interface it implements | Just make sure the methods exist |
| Mixing types from different libraries | Often requires adapters or wrappers | Works directly if the methods line up |
| Refactoring safety | Strong: rename an interface and the compiler tells you | Weak: rename a method and only tests catch it |
The trade is real. Duck typing buys flexibility at the cost of late-detected bugs. Nominal typing buys early detection at the cost of more upfront declarations. Python doesn't pick a side: duck typing is the default, but Protocol, ABC, and type hints are all available for stronger guarantees.
A small example shows the difference in practice. In a nominally-typed language, plugging in a third-party logger means writing an adapter that declares "this class implements my Logger interface". In Python, a logger with the right methods can be passed directly:
Neither logger inherits from anything special. The function works with both because both have info. A nominally-typed system would require a Logger interface that both classes declared. Here, the methods are the only contract.
Two style names show up everywhere in Python writing. EAFP stands for "Easier to Ask Forgiveness than Permission". LBYL stands for "Look Before You Leap". They describe two ways to handle the question "will this operation work?"
LBYL checks first and only attempts the operation if the check passes. EAFP tries the operation and catches the exception if it fails. EAFP is the natural style for duck-typed code, and using LBYL with duck typing undermines what duck typing is for.
Compare two ways to process a payment method that may or may not support refunds:
This works, but it has subtle problems. hasattr does its check by looking up the attribute and discarding any exception that comes out, so a property that raises on access is treated as "missing" without warning. If a future class implements refund as a property that throws when the user isn't authorized, the LBYL code reports "refunds not supported" instead of surfacing the real error.
The EAFP version sidesteps that problem:
Same output, different mechanics. EAFP only catches the exact failure it cares about, doesn't do the work twice (hasattr plus the actual call versus a single attempt), and isn't fooled by properties or __getattr__ implementations that have side effects.
There's a more practical reason EAFP fits duck typing: duck typing trusts that an object will respond to the right call. LBYL goes back to "check first whether this is a duck", which is the opposite of the duck-typing mindset. Checking the type first is closer to isinstance(obj, Duck) and nominal typing.
Raising and catching an exception is more expensive than not raising one. EAFP loses its edge in tight loops where failures are the common case; in normal code, the difference is invisible.
Duck typing is useful in three situations that come up constantly in Python code.
The first is flexible code that handles many input shapes. Functions that work with "any iterable" or "any file-like object" or "any callable" benefit directly. The standard library is full of them. sum, min, max, len, iter, list, tuple, set, sorted, all accept anything that fits a protocol. The result is that Python code composes well: data from one library flows into a function from another library without adapters.
Counter doesn't have separate constructors for lists, strings, and dicts. It expects an iterable, and every input here is one.
The second is testing. Duck typing makes test doubles trivial to write. If a function needs an object with .send(message), a test can pass a tiny class with a .send method that records what was sent. No mocking framework needed, no interface to declare, no inheritance to set up:
FakeNotifier doesn't inherit from anything. It has a send method that records, and notify_customer accepts it. A nominally-typed language would require a Notifier interface that both production and test classes implement, or a mocking library to generate the implementation.
The third is gluing libraries together. When two libraries weren't designed to know about each other, duck typing lets them cooperate as long as their methods happen to line up. A logging library that calls .info() and .error() on its input can accept a stdlib logging.Logger, a structlog logger, or a handwritten class. None of them inherit from anything in the logging library. They have the right methods.
Duck typing's costs are real, and ignoring them leads to bugs that hide until they're expensive to fix. Three failure modes come up often enough to be worth naming.
The first is failures from name mismatches that produce no error. A typo in a method name, or a subclass that names its method differently from the parent, produces wrong behavior instead of an error. The classic example is a missing override that falls through to the parent's method without any warning:
The total is wrong. HeavyAppliance doesn't override calculate_shipping; it defines a separate method called caclulate_shipping (with a typo) that no caller ever invokes. Every HeavyAppliance uses the parent's 4.99 shipping without any indication. No exception, no warning, only a broken total. A nominal type system would have caught this at compile time because the parent's interface declared calculate_shipping and the child failed to implement it.
The second is errors that surface far from their cause. Duck typing only finds out about a missing method when the method is called. If the call sits in a rarely-hit branch (a refund flow, a holiday-only promo, a particular admin tool), the bug can live in the code for months and only fire when a customer triggers the unlucky path:
The first call works because no GiftCard was in the cart. The second call fails at the moment calculate_shipping is invoked on the GiftCard, which could happen far from where the GiftCard was originally added to the cart. Debugging means tracing backwards from the exception to find which code path introduced an object that didn't fit the implicit contract.
The third is APIs that "work" but produce nonsense. Two methods might share a name and a signature but mean different things. A send method on a logger versus a send method on a socket: same name, very different semantics. Duck-typed code that calls obj.send(message) could accept either and produce output that looks reasonable on the surface.
notify_customer ran successfully both times. The second call did not email anyone; it wrote to a log. Duck typing has no way to express "this needs a sender, not a logger" beyond what the method name implies. The fix is usually clearer names, narrower interfaces, or a Protocol that pins down enough of the contract to make the misuse a type error.
Tests are the cheap fix for late duck-typing failures. A test that exercises each polymorphic branch catches the missing-method case before production does. Type hints (via Protocol or ABC) catch it earlier still, at type-check time.
Python offers several tools to recover the safety that duck typing trades away, without giving up the flexibility that makes duck typing useful in the first place. Three options cover most cases.
typing.Protocoltyping.Protocol (added in Python 3.8) describes a duck-typed contract that static type checkers can verify. A class inherits from Protocol and lists the methods and attributes the protocol requires. Any class with matching methods satisfies the protocol, without any inheritance:
Neither Book nor Tshirt inherits from Shippable. They satisfy the protocol because they have the right method. A type checker (mypy, pyright) catches a class without calculate_shipping at check time, before the code runs. Protocols keep duck typing's flexibility while letting a type checker verify the contract.
@runtime_checkable for isinstance ChecksBy default, a Protocol is a type-check-time construct only; isinstance(obj, Shippable) raises an error. The @runtime_checkable decorator makes isinstance work, but only by checking that the names exist (not the signatures):
@runtime_checkable gives a fast "does this object have the right method names?" check at runtime. It does not check that the method signature is right or that the return type is correct, which is why it's a backup, not a replacement, for a proper type check. Use it for input validation or fast-fail at API boundaries.
abc.ABC With Virtual SubclassesAbstract base classes give the strongest runtime guarantee: a class that doesn't implement the required methods can't be instantiated. The technique below makes ABC and @abstractmethod play with duck typing.
ABCMeta allows an existing class to be registered as a virtual subclass of an abstract base, without modifying that class:
ThirdPartyBook does not inherit from Shippable in any normal sense. The register call tells Python's ABC machinery to treat it as a subclass for the purposes of isinstance and issubclass checks. The class itself is unchanged. This is the bridge between duck typing and abstract base classes: the original class stays in use, it's registered once, and code that does isinstance(obj, Shippable) now accepts it.
The trade-off is that register doesn't check that ThirdPartyBook has the right methods. It's a promise to Python: "this class fits". If the class is missing a method, the failure mode is the same as without ABC: an AttributeError at call time. For full safety, combine register with tests, or prefer Protocol with @runtime_checkable when the check should run for real.
The diagram shows how virtual subclassing fits in: register updates a private set of "classes the ABC counts as subclasses", and isinstance consults that set in addition to the normal inheritance chain. The registered class itself doesn't change, doesn't gain a new base, and doesn't have to know it's been registered.
| Tool | What it provides | When it runs | Trade-off |
|---|---|---|---|
| Bare duck typing | Maximum flexibility, no setup | Runtime, at the call site | Errors surface late and far from the cause |
typing.Protocol | Structural typing the type checker verifies | Type-check time (mypy, pyright) | No runtime check by default |
@runtime_checkable Protocol | Adds an isinstance check for protocol membership | Runtime | Only checks method names, not signatures |
abc.ABC with subclassing | Runtime guarantee that abstract methods are implemented | Instantiation time | Requires explicit class C(ABC) declaration |
ABCMeta.register | Treat an existing class as a virtual subclass of an ABC | Registration time, then runtime | Doesn't verify methods; trusts the promise |
The Python ecosystem doesn't expect a single tool for every situation. Plain duck typing is fine for small functions and tests. Protocol is the default for typed code. ABC fits when the codebase controls the class hierarchy and a hard guarantee at instantiation is needed. register is the bridge that lets the strict tools work with classes the codebase doesn't own.
10 quizzes