Duck Typing
Why Protocols & ABCs Matters
The Problem: Duck typing is flexible but unchecked — call .quack() on the wrong thing and you find out at 3 AM.
The Solution: Abstract base classes enforce contracts at runtime; Protocol classes provide structural typing that mypy can verify without forcing inheritance.
Real Impact: Protocols are the right answer 90% of the time — they decouple your code from class hierarchies and let third-party types satisfy your interface implicitly.
Real-World Analogy
Think of Protocols as a job description and ABCs as a union card:
- Protocol = a job description — anyone with the right skills qualifies, no membership required
- ABC = a union card — you must explicitly join to do the job
- Duck typing = everyone walks in unverified — works until it doesn't
- @runtime_checkable = background check at hire time, not just at sign-up
Python's traditional approach: if it walks like a duck and quacks like a duck, it's a duck. We don't declare interfaces — any object with the right methods can be used.
def describe(thing):
# Works on anything with .name and .area()
print(f"{thing.name}: area = {thing.area()}")
class Circle:
name = "Circle"
def area(self): return 3.14
class Square:
name = "Square"
def area(self): return 1.0
describe(Circle())
describe(Square())
Duck typing is flexible but unchecked. Python offers two ways to formalize "behaves like X": abstract base classes and protocols.
Abstract Base Classes (ABCs)
An ABC declares which methods subclasses must implement. Instantiating an ABC directly — or a subclass that hasn't implemented all abstract methods — raises TypeError.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
# Concrete methods can call abstract ones
def describe(self) -> str:
return f"area={self.area()} perim={self.perimeter()}"
class Rect(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
def perimeter(self) -> float:
return 2 * (self.w + self.h)
Shape() # TypeError — can't instantiate abstract class
Rect(3, 4) # works
Built-in ABCs in collections.abc
Subclass these to inherit a rich set of mixin methods:
| ABC | You implement | You get |
|---|---|---|
Iterable | __iter__ | for x in obj |
Sized | __len__ | len(obj) |
Container | __contains__ | x in obj |
Sequence | __getitem__, __len__ | indexing, slicing, count, index, iteration |
MutableMapping | 5 methods | full dict-like behavior |
Protocols — Structural Typing (PEP 544)
Protocols define interfaces by structure rather than by inheritance. An object satisfies a Protocol if it has the right attributes and methods — no explicit subclassing needed. This is duck typing with type-checker support.
from typing import Protocol
class SupportsArea(Protocol):
def area(self) -> float: ...
def total_area(shapes: list[SupportsArea]) -> float:
return sum(s.area() for s in shapes)
# These satisfy the protocol without subclassing
class Circle:
def area(self): return 3.14
class Rect:
def area(self): return 1.0
total_area([Circle(), Rect()]) # type-checks, runs fine
Protocols vs ABCs
Use Protocol when consumers shouldn't need to know about your interface (typing library code, third-party types). Use ABC when you want explicit subclass enforcement at runtime.
Runtime Checks with @runtime_checkable
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
isinstance(open("/tmp/x"), Closable) # True
Built-in Protocols from typing
The typing module ships protocols for common patterns — use these in annotations.
| Protocol | Means |
|---|---|
Iterable[T] | Supports iter() |
Iterator[T] | Supports iter() + next() |
Sequence[T] | Indexable + sized |
Mapping[K, V] | Dict-like |
Callable[[A, B], R] | Function-like |
Hashable | Has __hash__ |
SupportsInt/Float/Str | Can be converted |
from typing import Iterable, Mapping
def total(prices: Iterable[float]) -> float:
return sum(prices)
def to_html(attrs: Mapping[str, str]) -> str:
return " ".join(f'{k}="{v}"' for k, v in attrs.items())
🎯 Practice Exercises
Exercise 1: Drawable protocol
Define a Drawable Protocol with draw() -> str. Write render(items: list[Drawable]) that joins their output.
Exercise 2: Custom Sequence
Subclass collections.abc.Sequence to build a Range class. Implement __getitem__ and __len__. You get iteration, count, index, and in for free.
Exercise 3: Abstract Repository
Define an ABC Repository[T] with abstract save, find, delete. Implement a MemoryRepository and verify isinstance works.