Introduction
Protocol classes from typing define interfaces for structural subtyping. Unlike nominal subtyping, protocols check interface compatibility at runtime.
Basic Protocol
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None:
...
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(item: Drawable):
item.draw()
render(Circle())
render(Square())
Protocol with Type Checking
from typing import Protocol, runtime_checkable
@runtime_checkable
class Comparable(Protocol):
def __lt__(self, other: "Comparable") -> bool:
...
class Integer:
def __init__(self, value: int):
self.value = value
def __lt__(self, other: "Integer") -> bool:
return self.value < other.value
a = Integer(1)
b = Integer(2)
print(a < b) # True
isinstance(a, Comparable) # True
Protocol Inheritance
from typing import Protocol
class Printable(Protocol):
def print(self) -> None:
...
class Serializable(Protocol):
def to_json(self) -> str:
...
class Document(Printable, Serializable):
def print(self) -> None:
pass
def to_json(self) -> str:
return "{}"
Generic Protocols
from typing import TypeVar, Generic, Protocol
T = TypeVar("T")
class Container(Protocol[T]):
def get(self) -> T:
...
class Box(Generic[T]):
def __init__(self, item: T):
self._item = item
def get(self) -> T:
return self._item
box: Box[int] = Box(42)
container: Container[int] = box
Protocol for Duck Typing
from typing import Iterator, Iterable
def sum_all(numbers: Iterable[int]) -> int:
return sum(numbers)
class NumberGenerator:
def __iter__(self):
return iter([1, 2, 3])
result = sum_all(NumberGenerator()) # Works with any iterable
Practice Problems
- Create a protocol for file-like objects
- Implement generic protocol with bounds
- Use runtime_checkable protocol
- Define protocol for database operations
- Create protocol composition