PEP 544: Protocols - Structural Subtyping (Static Duck Typing)
TL;DR
PEP 544 introduces Protocol classes that enable structural subtyping (static duck typing) - type checking based on what methods an object has rather than its inheritance hierarchy, making Python’s type system more flexible and duck-typing friendly.
Interesting!
Protocols let you define “interfaces” that classes can implement just by having the right methods - no explicit inheritance needed! A class becomes a valid protocol implementation simply by having the required methods with correct signatures.
What Are Protocols?
Structural vs Nominal Typing
python code snippet start
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
# No inheritance needed!
class Circle:
def draw(self) -> None:
print("Drawing a circle")
class Square:
def draw(self) -> None:
print("Drawing a square")
# Both work with functions expecting Drawable
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # ✓ Works - has draw() method
render(Square()) # ✓ Works - has draw() method
python code snippet end
Generic Protocols
Iterator Protocol Example
python code snippet start
from typing import Protocol, TypeVar, Iterator
T = TypeVar('T')
class Iterable(Protocol[T]):
def __iter__(self) -> Iterator[T]:
...
class NumberRange:
def __init__(self, start: int, end: int):
self.start = start
self.end = end
def __iter__(self) -> Iterator[int]:
current = self.start
while current < self.end:
yield current
current += 1
# NumberRange automatically satisfies Iterable[int]
def process_numbers(items: Iterable[int]) -> int:
return sum(items)
result = process_numbers(NumberRange(1, 5)) # ✓ Type-safe
python code snippet end
Runtime Checking
@runtime_checkable Decorator
python code snippet start
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Circle")
class NotDrawable:
pass
# Runtime checking
print(isinstance(Circle(), Drawable)) # True
print(isinstance(NotDrawable(), Drawable)) # False
# Type-safe checking
if isinstance(obj, Drawable):
obj.draw() # Type checker knows obj has draw()
python code snippet end
Real-World Protocol Examples
File-Like Protocol
python code snippet start
from typing import Protocol
class Readable(Protocol):
def read(self, size: int = -1) -> str:
...
class Writable(Protocol):
def write(self, data: str) -> int:
...
# Works with files, StringIO, custom classes
def copy_text(src: Readable, dst: Writable) -> None:
data = src.read()
dst.write(data)
# Usage
import io
copy_text(open('input.txt'), open('output.txt', 'w'))
copy_text(io.StringIO('hello'), io.StringIO())
python code snippet end
Context Manager Protocol
python code snippet start
class ContextManager(Protocol[T]):
def __enter__(self) -> T:
...
def __exit__(self, exc_type, exc_value, traceback) -> bool | None:
...
class DatabaseConnection:
def __enter__(self) -> 'DatabaseConnection':
print("Connecting to database")
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
print("Closing database connection")
# Automatically works with 'with' statements
with DatabaseConnection() as db:
pass # Use database
python code snippet end
Benefits of Protocols
Flexibility Without Inheritance
python code snippet start
# Traditional approach - rigid inheritance
from abc import ABC, abstractmethod
class Animal(ABC):
@abstractmethod
def make_sound(self) -> str:
pass
class Dog(Animal): # Must inherit
def make_sound(self) -> str:
return "Woof!"
# Protocol approach - structural typing
class SoundMaker(Protocol):
def make_sound(self) -> str:
...
class Robot: # No inheritance needed!
def make_sound(self) -> str:
return "Beep!"
def hear_sound(thing: SoundMaker) -> None:
print(thing.make_sound())
hear_sound(Robot()) # ✓ Works without inheritance
python code snippet end
Best Practices
- Use protocols for defining interfaces in libraries
- Combine with
@runtime_checkable
when runtime type checking is needed - Keep protocol methods minimal and focused
- Use generic protocols for reusable interfaces
- Protocols work great with dependency injection
Protocols bring the best of both worlds: Python’s duck typing flexibility with static type checking safety!
Reference: PEP 544 - Protocols