typing.Protocol — Structural Subtyping¶
Protocol (Python 3.8+) enables structural subtyping (static duck typing). Classes don't need to inherit from a Protocol—they just need to implement the required methods.
from typing import Protocol
Nominal vs Structural Typing¶
Nominal Typing (ABC)¶
Classes must explicitly inherit:
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self) -> str:
pass
# Must inherit from Drawable
class Circle(Drawable):
def draw(self) -> str:
return "○"
# This class has draw() but ISN'T Drawable
class Square:
def draw(self) -> str:
return "□"
def render(shape: Drawable):
print(shape.draw())
render(Circle()) # ✓ OK
render(Square()) # ✗ Type error (doesn't inherit Drawable)
Structural Typing (Protocol)¶
Classes just need matching methods:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
# No inheritance needed!
class Circle:
def draw(self) -> str:
return "○"
class Square:
def draw(self) -> str:
return "□"
def render(shape: Drawable):
print(shape.draw())
render(Circle()) # ✓ OK
render(Square()) # ✓ OK - has draw(), so it's Drawable!
Defining Protocols¶
Basic Protocol¶
from typing import Protocol
class Greeter(Protocol):
def greet(self, name: str) -> str:
... # Use ... or pass
# Any class with greet(name: str) -> str works
class FormalGreeter:
def greet(self, name: str) -> str:
return f"Good day, {name}."
class CasualGreeter:
def greet(self, name: str) -> str:
return f"Hey {name}!"
def say_hello(greeter: Greeter, name: str):
print(greeter.greet(name))
say_hello(FormalGreeter(), "Alice") # "Good day, Alice."
say_hello(CasualGreeter(), "Bob") # "Hey Bob!"
Protocol with Multiple Methods¶
from typing import Protocol
class Database(Protocol):
def connect(self) -> None:
...
def execute(self, query: str) -> list:
...
def close(self) -> None:
...
# Must implement ALL methods to be compatible
class PostgresDB:
def connect(self) -> None:
print("Connecting to Postgres...")
def execute(self, query: str) -> list:
return [{"id": 1}]
def close(self) -> None:
print("Closing connection")
def run_query(db: Database, query: str) -> list:
db.connect()
result = db.execute(query)
db.close()
return result
Protocol with Properties¶
from typing import Protocol
class Named(Protocol):
@property
def name(self) -> str:
...
class Person:
def __init__(self, name: str):
self._name = name
@property
def name(self) -> str:
return self._name
class Company:
name: str # Class attribute also satisfies protocol
def __init__(self, name: str):
self.name = name
def display(obj: Named):
print(f"Name: {obj.name}")
display(Person("Alice")) # ✓
display(Company("Acme")) # ✓
Protocol with Class Variables¶
from typing import Protocol, ClassVar
class Configurable(Protocol):
config_key: ClassVar[str]
def configure(self) -> None:
...
class DatabaseConfig:
config_key: ClassVar[str] = "database"
def configure(self) -> None:
print(f"Configuring {self.config_key}")
Runtime Checking¶
By default, Protocols only work for static type checking. For runtime isinstance() checks:
@runtime_checkable¶
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None:
...
class File:
def close(self) -> None:
print("File closed")
class Connection:
def close(self) -> None:
print("Connection closed")
f = File()
print(isinstance(f, Closeable)) # True
# Works with any object that has close()
import io
buffer = io.StringIO()
print(isinstance(buffer, Closeable)) # True
Limitations of Runtime Checking¶
@runtime_checkable only checks method existence, not signatures:
@runtime_checkable
class Adder(Protocol):
def add(self, x: int, y: int) -> int:
...
class BadAdder:
def add(self): # Wrong signature!
return 42
# Runtime check passes (only checks if 'add' exists)
print(isinstance(BadAdder(), Adder)) # True!
# But type checker would catch this
Inheriting from Protocol¶
Extending Protocols¶
from typing import Protocol
class Reader(Protocol):
def read(self) -> str:
...
class Writer(Protocol):
def write(self, data: str) -> None:
...
class ReadWriter(Reader, Writer, Protocol):
"""Combines Reader and Writer."""
pass
# Must implement both read() and write()
class File:
def read(self) -> str:
return "data"
def write(self, data: str) -> None:
print(f"Writing: {data}")
def process(rw: ReadWriter):
data = rw.read()
rw.write(data.upper())
process(File()) # ✓
Adding Methods to Extended Protocol¶
from typing import Protocol
class Identifiable(Protocol):
@property
def id(self) -> int:
...
class Timestamped(Protocol):
@property
def created_at(self) -> str:
...
class Entity(Identifiable, Timestamped, Protocol):
@property
def updated_at(self) -> str:
...
Generic Protocols¶
from typing import Protocol, TypeVar
T = TypeVar('T')
class Container(Protocol[T]):
def get(self) -> T:
...
def set(self, value: T) -> None:
...
class Box:
def __init__(self, value: int):
self._value = value
def get(self) -> int:
return self._value
def set(self, value: int) -> None:
self._value = value
def double_value(container: Container[int]) -> None:
container.set(container.get() * 2)
box = Box(5)
double_value(box)
print(box.get()) # 10
Covariant and Contravariant¶
from typing import Protocol, TypeVar
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
class Readable(Protocol[T_co]):
def read(self) -> T_co:
...
class Writable(Protocol[T_contra]):
def write(self, value: T_contra) -> None:
...
Practical Examples¶
Callback Protocol¶
from typing import Protocol
class EventHandler(Protocol):
def __call__(self, event: str, data: dict) -> None:
...
def on_click(event: str, data: dict) -> None:
print(f"Clicked: {event}, {data}")
class ClickLogger:
def __call__(self, event: str, data: dict) -> None:
print(f"[LOG] {event}: {data}")
def register_handler(handler: EventHandler):
handler("click", {"x": 10, "y": 20})
register_handler(on_click) # Function ✓
register_handler(ClickLogger()) # Callable object ✓
Iterator Protocol¶
from typing import Protocol, TypeVar, Iterator
T = TypeVar('T')
class SupportsIter(Protocol[T]):
def __iter__(self) -> Iterator[T]:
...
def first_item(items: SupportsIter[T]) -> T:
return next(iter(items))
# Works with any iterable
print(first_item([1, 2, 3])) # 1
print(first_item({4, 5, 6})) # 4 (or 5 or 6)
print(first_item("hello")) # 'h'
Comparable Protocol¶
from typing import Protocol, TypeVar
T = TypeVar('T')
class Comparable(Protocol):
def __lt__(self, other: 'Comparable') -> bool:
...
def __eq__(self, other: object) -> bool:
...
def min_value(a: Comparable, b: Comparable) -> Comparable:
return a if a < b else b
# Works with any comparable
print(min_value(3, 7)) # 3
print(min_value("apple", "banana")) # "apple"
Context Manager Protocol¶
from typing import Protocol, TypeVar
T = TypeVar('T')
class ContextManager(Protocol[T]):
def __enter__(self) -> T:
...
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
...
class Timer:
def __enter__(self) -> 'Timer':
import time
self.start = time.time()
return self
def __exit__(self, *args) -> bool:
import time
self.elapsed = time.time() - self.start
return False
def timed_operation(cm: ContextManager[Timer]):
with cm as timer:
pass
return timer.elapsed
Protocol vs ABC Comparison¶
| Feature | Protocol | ABC |
|---|---|---|
| Inheritance required | No | Yes |
| Static type checking | ✓ | ✓ |
| Runtime isinstance() | With @runtime_checkable |
Always |
| Method implementation | Not enforced | Enforced |
| Signature checking (runtime) | No | No |
| Use case | Duck typing, interfaces | Contracts, mixins |
When to Use Each¶
Use Protocol when: - You want duck typing with type hints - Working with external code you can't modify - Defining interfaces for callbacks - Type checking without requiring inheritance
Use ABC when:
- You need runtime enforcement
- You want to share implementation (concrete methods)
- Building class hierarchies
- Need isinstance() checks without decorator
Summary¶
| Feature | Syntax |
|---|---|
| Define Protocol | class MyProtocol(Protocol): |
| Runtime checkable | @runtime_checkable |
| Method stub | def method(self) -> None: ... |
| Property | @property + ... |
| Extend Protocol | class Extended(Proto1, Proto2, Protocol): |
| Generic Protocol | class Container(Protocol[T]): |
Key Takeaways:
- Protocols enable structural subtyping (static duck typing)
- No inheritance required—just implement the methods
- Use
@runtime_checkableforisinstance()support - Runtime checks only verify method existence, not signatures
- Protocols are ideal for type hints without tight coupling
- Prefer Protocol for interfaces, ABC for enforced contracts