Structural Subtyping with Protocol
debt(d5/e3/b3/t6)
Closest to 'specialist tool catches' (d5), mypy/pyright detect Protocol mismatches and missing methods at type-check time, but without these tools structural mismatches are silent.
Closest to 'simple parameterised fix' (e3), replacing an ABC with a Protocol or adding @runtime_checkable is a localized change in the type definition, though implementing classes don't need changes.
Closest to 'localised tax' (b3), Protocols are typically defined in one module and used by consumers; they don't pervade the architecture but do shape API contracts for that component.
Closest to 'serious trap' (t7), the misconception that Protocol requires explicit registration like ABC contradicts how interfaces work in most other languages (Java/PHP/C#), and the runtime vs. type-check distinction with @runtime_checkable is a notable additional gotcha.
Also Known As
TL;DR
Explanation
Protocol (typing.Protocol, Python 3.8+): a class satisfies a Protocol if it has all required attributes and methods, regardless of inheritance. Unlike ABC (which requires explicit subclassing), Protocol works at type-check time without modifying existing classes. @runtime_checkable enables isinstance() checks at runtime. Use for: typing third-party classes you cannot modify, documenting expected interfaces without coupling to a class hierarchy, and expressing duck types with static type safety.
Common Misconception
Why It Matters
Common Mistakes
- Using Protocol when ABC is more appropriate — use ABC when you want enforcement at class definition time
- Not adding @runtime_checkable when isinstance() checks are needed
- Protocol with mutable attributes — structural subtyping with mutable attributes can be surprising
- Forgetting that satisfying the Protocol is checked at type-check time not runtime (without @runtime_checkable)
Code Examples
# Any type — loses all type safety:
def process(reader: Any) -> str:
return reader.read() # No type checking — any attribute access allowed
# ABC — requires modifying existing classes:
from abc import ABC, abstractmethod
class Readable(ABC):
@abstractmethod
def read(self) -> str: ...
# All implementing classes must inherit Readable — invasive coupling
from typing import Protocol, runtime_checkable
@runtime_checkable
class Readable(Protocol):
def read(self) -> str: ...
def process(reader: Readable) -> str:
return reader.read() # Type-checked — must have read() -> str
# Any class with read() -> str satisfies it — no inheritance needed:
class FileReader:
def read(self) -> str: return open(self.path).read()
class MockReader:
def read(self) -> str: return 'mock data'
process(FileReader()) # Type-safe
process(MockReader()) # Type-safe
isinstance(MockReader(), Readable) # True (runtime_checkable)