Protocols (PEP 544) — Structural Subtyping
Python · Advanced · Run with python3
Explanation
`typing.Protocol` (Python 3.8+) defines structural interfaces — any class that implements the required methods satisfies the protocol, without explicit inheritance. This is static duck typing: `mypy` and `pyright` verify compliance at type-check time, not runtime. Use `@runtime_checkable` to enable `isinstance` checks. Protocols capture what code *does* rather than what it *inherits* — perfect for third-party classes you can't modify.
Real-World Scenario
A logging system accepts any object with a `write(message: str) -> None` method. `class Writable(Protocol): def write(self, msg: str) -> None: ...` lets `sys.stdout`, `io.StringIO`, a custom `SlackSink`, and a no-op `DevNull` all work with the same type-annotated code. ```python from typing import Protocol, runtime_checkable @runtime_checkable class Drawable(Protocol): def draw(self) -> str: ... def area(self) -> float: ... class Circle: def __init__(self, r: float): self.r = r def draw(self) -> str: return f"Circle(r={self.r})" def area(self) -> float: return 3.14159 * self.r ** 2 class Rectangle: def __init__(self, w: float, h: float): self.w, self.h = w, h def draw(self) -> str: return f"Rect({self.w}x{self.h})" def area(self) -> float: return self.w * self.h def render(shape: Drawable) -> None: print(f"{shape.draw()} — area: {shape.area():.2f}") shapes: list[Drawable] = [Circle(5), Rectangle(3, 4)] for s in shapes: render(s) print(isinstance(Circle(1), Drawable)) # True (runtime_checkable) ```
Quick Snippet
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ...
def area(self) -> float: ...
class Circle:
def __init__(self, r: float): self.r = r
def draw(self) -> str: return f"Circle(r={self.r})"
def area(self) -> float: return 3.14159 * self.r ** 2
class Rectangle:
def __init__(self, w: float, h: float): self.w, self.h = w, h
def draw(self) -> str: return f"Rect({self.w}x{self.h})"
def area(self) -> float: return self.w * self.h
def render(shape: Drawable) -> None:
print(f"{shape.draw()} — area: {shape.area():.2f}")
shapes: list[Drawable] = [Circle(5), Rectangle(3, 4)]
for s in shapes:
render(s)
print(isinstance(Circle(1), Drawable)) # True (runtime_checkable)Full Example
# Compile: python3 protocols.py
# Output:
# Circle(r=5.0) — area: 78.54
# Rect(3.0x4.0) — area: 12.00
# True
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> str: ...
def area(self) -> float: ...
class Circle:
def __init__(self, r: float): self.r = r
def draw(self) -> str: return f"Circle(r={self.r})"
def area(self) -> float: return 3.14159 * self.r ** 2
class Rectangle:
def __init__(self, w: float, h: float): self.w, self.h = w, h
def draw(self) -> str: return f"Rect({self.w}x{self.h})"
def area(self) -> float: return self.w * self.h
def render(shape: Drawable) -> None:
print(f"{shape.draw()} — area: {shape.area():.2f}")
shapes: list[Drawable] = [Circle(5), Rectangle(3, 4)]
for s in shapes:
render(s)
print(isinstance(Circle(1), Drawable)) # True
print(isinstance("not drawable", Drawable)) # False
python3 protocols.py