Protocols & Structural Typing
typing.Protocol enables static duck typing: any class with matching methods satisfies the protocol without subclassing. This is the typing-layer companion to OOP structural patterns.
Recipe
from typing import Protocol
class SupportsWrite(Protocol):
def write(self, data: bytes) -> int: ...
def persist(stream: SupportsWrite, payload: bytes) -> None:
stream.write(payload)When to reach for this:
- Typing file-like objects and streams
- Defining ports in hexagonal architecture
- Testing with fakes without ABC inheritance
- Gradual typing on duck-typed legacy code
Working Example
from typing import Protocol, runtime_checkable
@runtime_checkable
class Repository(Protocol):
def get(self, item_id: int) -> dict[str, object]: ...
def save(self, item: dict[str, object]) -> None: ...
class InMemoryRepo:
def __init__(self) -> None:
self._data: dict[int, dict[str, object]] = {}
def get(self, item_id: int) -> dict[str, object]:
return self._data[item_id]
def save(self, item: dict[str, object]) -> None:
self._data[int(item["id"])] = item
def service(repo: Repository) -> None:
repo.save({"id": 1, "name": "Ada"})
assert repo.get(1)["name"] == "Ada"
if __name__ == "__main__":
service(InMemoryRepo())
print(isinstance(InMemoryRepo(), Repository))What this demonstrates:
InMemoryReposatisfiesRepositorywithout inheritance- Service depends on structural interface
@runtime_checkableenables isinstance in tests- Methods use
...ellipsis bodies in Protocol definition
Deep Dive
How It Works
- PEP 544 - Structural subtyping for static checkers.
- Nominal vs structural - ABC requires inheritance; Protocol does not.
- runtime_checkable - isinstance checks method names only - shallow.
- Protocol inheritance - Extend protocols to add methods.
- @property in Protocol - Declare read-only attributes structurally.
Protocol vs ABC
| Need | Pick |
|---|---|
| Third-party structural match | Protocol |
| Enforce at instantiation | ABC |
| isinstance deep validation | Neither alone - tests + types |
Python Notes
class Readable(Protocol):
def read(self, n: int = -1) -> bytes: ...
# Generic protocol
from typing import TypeVar
T = TypeVar("T", contravariant=True)
class Box(Protocol[T]):
def put(self, item: T) -> None: ...Gotchas
- isinstance false confidence - runtime_checkable shallow. Fix: Do not rely for security boundaries.
- Protocol runtime inheritance - Typing only - do not expect Protocol base behavior at runtime.
- Private methods - Protocol cannot see name-mangled methods. Fix: Public interface methods only.
- Structural drift - Implementation changes break checker silently at call sites. Fix: CI mypy on both sides.
- Duplicate Protocol definitions - Structural equivalence not automatic across modules. Fix: Import shared Protocol type.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| ABC | Control inheritance tree | Third-party types |
object | No static help | Need method contracts |
| zope.interface | Legacy ecosystem | New typing-first code |
| single concrete class | One implementation | Multiple shapes |
FAQs
inherit Protocol?
Optional for typing clarity; not required for structural match at runtime.
Protocol with properties?
Declare @property def x(self) -> int: ... in protocol body.
pyright protocol support?
Strong - often stricter than mypy on protocol overlap edge cases.
Protocol for callables?
Use Protocol with __call__ or Callable - Callable simpler for functions only.
total_match?
Not built-in - ensure all protocol methods implemented; checkers report missing.
numpy array protocol?
Libraries define Protocols matching ndarray duck API without importing numpy in types-only stub.
FastAPI Depends typing?
Depend on Protocol-typed services for test doubles in route functions.
frozen protocol?
Protocols themselves do not enforce immutability - document implementation expectations.
typing_extensions?
3.14 has stdlib Protocol; older versions used typing_extensions backport.
overlap with OOP article?
OOP article focuses runtime patterns; this page focuses static checker configuration and API design.
Related
- Protocols & Structural Typing - runtime OOP view
- Abstract Base Classes - nominal alternative
- Generics & TypeVar - generic protocols
- mypy & pyright Setup - checker config
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.