Mental model
Python supports both nominal subtyping (Abstract Base Classes requiring explicit inheritance class Custom(ABC)) and structural subtyping (typing.Protocol requiring only matching interface shapes without inheritance).
Theory
A Protocol (PEP 544) defines a contract by interface shape rather than explicit class hierarchy. Any class that implements the methods and attributes defined in a Protocol is considered a valid subtype by static type checkers (mypy, Pyright), embodying static "duck typing" without tight coupling.
Alternatives and trade-offs
abc.ABC(Nominal): Enforces method implementation at runtime instantiation (TypeErrorif missing).typing.Protocol(Structural): Checked statically at compile time without runtime inheritance overhead.
Failure modes and misconceptions
Protocolruntime checks: By default,isinstance(obj, Protocol)raises aTypeErrorunless annotated with@runtime_checkable.- Metaclass over-engineering: Using custom metaclasses for simple validation adds unnecessary magic; use Pydantic models or Protocols instead.
Decision scenario
Use typing.Protocol to define contracts for third-party pluggable adapters (database clients, notification channels) so third-party teams do not need to inherit from your internal base classes.
Learning outcomes
- Contrast structural subtyping (
typing.Protocol) with nominal subtyping (abc.ABC). - Enable runtime
isinstance()checks using@runtime_checkable. - Design decoupled, testable Python systems with static type guarantees.
Trade-offs
Protocols enable static duck typing without explicit inheritance coupling, but require static type checkers (mypy/pyright) in CI pipelines to enforce contracts.