skip to content

Protocol vs ABC and Duck Typing

Choosing between a structural Protocol and a nominal abc.ABC, and how both relate to plain duck typing. Interviewers ask which one keeps the dependency arrow pointing the way you want.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does duck typing mean in Python, and when does a missing method actually fail?

level: juniorimportance: must knowfreq 65%

answer

  1. Objects are judged by what they provide
  2. No check happens when you pass it
  3. The error is raised at call time
  4. AttributeError, possibly after partial work
  5. EAFP; dunders resolve on the type

basics

~20 s

Duck typing means Python cares about the attributes an object actually has, not the class it inherits from. Nothing is verified when you pass the object; the AttributeError arrives only at the moment the missing method is looked up and called.

solid answer

~40 s

Duck typing is how attribute access already works in CPython: `thing.write(line)` looks `write` up on the instance and then along its type's method resolution order, and calls whatever it finds. No class, base class or annotation is consulted, so any object exposing a compatible `write` is accepted. The cost is that the check happens at the point of use: a function can run half its work and then raise `AttributeError` on the first line that touches the missing member. Annotations do not change this — they are erased as far as the interpreter is concerned. If you want the mismatch reported earlier you bolt something on: an `abc.ABC` base class enforces it at instantiation time, and a `typing.Protocol` lets a static type checker flag it before the code ever runs.

code

python · 18 lines
python
class Duck:
    def speak(self) -> str:
        return "quack"

class Robot:
    def speak(self) -> str:
        return "beep"

def announce(thing) -> None:
    print(thing.speak())

for obj in (Duck(), Robot()):
    announce(obj)

try:
    announce(object())
except AttributeError as exc:
    print("failed only when called:", exc)

go deeper

for a junior

Be ready to define duck typing in one sentence and to say exactly when the failure happens: at the attribute lookup, as an AttributeError, not at the moment the object is passed in.

for a middle

Explain the mechanics — lookup through the instance then the type's MRO, annotations as unenforced metadata, and the fact that special methods like iter resolve on the type rather than the instance.

for a senior

Show the production consequence: late failures can leave half-finished work behind, so you argue for pushing the check earlier with a Protocol checked in CI or an abstract base class, and for narrow contracts.

for a principal

Own the tradeoff between the flexibility that makes Python code composable and the cost of contracts that exist only inside function bodies, and set the team convention for where interfaces get written down.

## What duck typing describes **Duck typing is not a feature; it is a description of how attribute lookup already behaves.** When CPython evaluates `thing.write(line)` it does not consult a declared type for `thing`. It looks `write` up in the instance dictionary, then walks the type's method resolution order, and calls the first thing it finds. Nothing anywhere along that path asked which class `thing` came from. The slogan — if it walks like a duck and quacks like a duck, treat it as a duck — is a compressed way of saying that **conformance is decided at the point of use**, by what the object provides, not by what it inherits. ## Three consequences Three consequences fall out of that, and interviewers are usually probing for the second and third. 1. **Nothing is checked at the boundary.** Calling `emit(sink)` binds a name to an object; it verifies nothing. If `sink` has no `write`, `emit` runs happily until the statement that reaches for it, and only then raises `AttributeError: 'Robot' object has no attribute 'write'`. Type annotations do not help here: at runtime a parameter annotated `sink: LineSink` accepts literally anything, because annotations are metadata for tools, not a runtime gate. 2. **Failure is late, and can be partial.** Because the error surfaces mid-call, the function may already have appended to a list, opened a file, or written half a batch of samples before it discovers the object is wrong. A nominal language would have rejected the argument at the boundary; Python discovers it in the middle. That asymmetry — flexible in, unpredictable when it goes wrong — is the real tradeoff of duck typing and the reason the two "check it earlier" mechanisms exist. 3. **Only the *shape you actually touch* matters.** A function that calls only `write` will accept an object with nothing else on it. That is why Python code passes file-like objects around so casually: `read` and `write` are the whole contract in practice. It also means the contract is invisible unless someone writes it down — the set of members a function requires lives in its body, not in its signature. ## Two idioms Two idioms grow out of this. - **EAFP** (easier to ask forgiveness than permission) is the idiomatic one: just call the method and handle `AttributeError` or `TypeError` if it is absent. - **LBYL** (look before you leap) checks first with `hasattr(obj, "write")` or `getattr(obj, "write", None)`, which is the right shape when a capability is genuinely optional — for example, calling `close` only if the object has one. ## The exception: special methods One important exception to "lookup happens on the instance": **special methods are looked up on the type, not on the instance.** `len(x)` finds `__len__` on `type(x)`; assigning `x.__len__ = lambda: 3` on an instance does not make `len(x)` work. So the implicit protocols the language itself uses — iteration via `__iter__`, context managers via `__enter__` and `__exit__`, arithmetic via the operator dunders — are duck typed against the class, and failures there show up as `TypeError` rather than `AttributeError`. The standard library also ships a *runtime* structural check for a handful of these: several abstract base classes in `collections.abc` define `__subclasshook__`, so `isinstance(obj, collections.abc.Iterable)` is `True` for any class defining `__iter__`, with no inheritance or registration at all. That is duck typing promoted to an `isinstance` answer, and it is why those checks are cheap to reach for. ## Moving the failure earlier Finally, the two ways to move the failure earlier, which is the setup for the Protocol-versus-ABC discussion. - An `abc.ABC` with `abc.abstractmethod` members refuses to instantiate a subclass that has not implemented them, raising `TypeError` at construction — early, but it requires the implementer to inherit. - A `typing.Protocol` (added in Python 3.8) describes the same shape, and a static type checker reports the mismatch before the program runs — early, and with no inheritance required. Neither one changes runtime behaviour of attribute access: duck typing is still what happens when the code executes. They only change *when you find out*.

  • Does annotating a parameter with a class name make Python reject a wrong argument at runtime?
    No. Annotations are stored as metadata and are not enforced by the interpreter, so an annotated parameter still accepts any object. The annotation is read by static type checkers and by libraries that choose to inspect it. If you want a runtime guarantee you have to write the check yourself, with an explicit `isinstance` test or by requiring an `abc.ABC` subclass.
  • Why does `len(x)` fail even when the instance has a `__len__` attribute assigned to it?
    Implicit special-method lookup goes through the type, not the instance dictionary. `len(x)` looks for `__len__` on `type(x)`, so an attribute set on the instance is ignored and you get `TypeError: object of type 'X' has no len()`. The same rule applies to `__iter__`, `__enter__` and the operator dunders — to change that behaviour you set the method on the class.
  • When is `hasattr` the right check rather than just calling the method?
    When the capability is genuinely optional and the absence is not an error — for example calling `close` only on objects that have one, or taking a fast path when an object exposes it. For the normal case, calling and catching is preferred: `hasattr` swallows any exception raised by the attribute lookup itself and adds a second lookup, and it invites a check that drifts out of sync with the call below it.

A duck-typed function is a doorman who never asks for ID and only notices you cannot dance once the music is already playing.

saying these in an interview costs you the question

  • Claims type annotations are enforced at runtime
  • Says the error is raised when the argument is passed
  • Thinks duck typing requires a common base class
  • Confuses duck typing with dynamic attribute creation
  • Believes an instance-level __len__ makes len() work

context

open as a page

How does typing.Protocol differ from abc.ABC when you define an interface?

level: middleimportance: must knowfreq 50%

basics

~20 s

An abc.ABC is nominal: a class conforms only by inheriting from it, and the interpreter enforces that at instantiation. A typing.Protocol is structural: any class with matching members conforms, checked by a static type checker with no inheritance and no runtime cost.

open as a page

Why define the sink interface as a Protocol inside your metrics scraper rather than an ABC every sink provider imports?

level: seniorimportance: should knowfreq 32%

basics

~20 s

A Protocol declared in the scraper reverses the dependency: providers need no import and no inheritance, so pre-existing and standard-library objects fit, and each consumer states only the members it calls. An ABC would force every provider to depend on the scraper package.

open as a page

When should a shared library publish Protocols instead of ABCs as its extension points, and who owns them?

level: principalimportance: should knowfreq 26%

basics

~20 s

Publish an abc.ABC when you need runtime enforcement, isinstance dispatch or shared implementation; publish a typing.Protocol when implementers should not depend on you. The deciding factor is enforcement: a Protocol is only as real as the type-checking gate in CI.

open as a page