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.
Search across all documentation pages
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.
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:
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:
InMemoryRepo satisfies Repository without inheritance@runtime_checkable enables isinstance in tests... ellipsis bodies in Protocol definition| Need | Pick |
|---|---|
| Third-party structural match | Protocol |
| Enforce at instantiation | ABC |
| isinstance deep validation | Neither alone - tests + types |
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: ...| 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 |
Optional for typing clarity; not required for structural match at runtime.
Declare @property def x(self) -> int: ... in protocol body.
Strong - often stricter than mypy on protocol overlap edge cases.
Use Protocol with __call__ or Callable - Callable simpler for functions only.
Not built-in - ensure all protocol methods implemented; checkers report missing.
Libraries define Protocols matching ndarray duck API without importing numpy in types-only stub.
Depend on Protocol-typed services for test doubles in route functions.
Protocols themselves do not enforce immutability - document implementation expectations.
3.14 has stdlib Protocol; older versions used typing_extensions backport.
OOP article focuses runtime patterns; this page focuses static checker configuration and API design.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026