Protocols enable structural subtyping—if an object has the right methods, it satisfies the protocol. No inheritance required. Duck typing with type safety.
Basic Protocol
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
# No inheritance needed!
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Square:
def draw(self) -> None:
print("Drawing square")
def render(shape: Drawable) -> None:
shape.draw()
# Both work - they have draw()
render(Circle()) # OK
render(Square()) # OKProtocol vs ABC
from typing import Protocol
from abc import ABC, abstractmethod
# ABC requires inheritance
class DrawableABC(ABC):
@abstractmethod
def draw(self) -> None: ...
class Shape(DrawableABC):
def draw(self) -> None:
print("Shape")
# Protocol doesn't
class DrawableProtocol(Protocol):
def draw(self) -> None: ...
# Any class with draw() works
class ExternalWidget: # Third-party class
def draw(self) -> None:
print("Widget")
def render(d: DrawableProtocol) -> None:
d.draw()
render(ExternalWidget()) # Works!Protocol with Properties
from typing import Protocol
class Named(Protocol):
@property
def name(self) -> str: ...
class User:
def __init__(self, name: str):
self._name = name
@property
def name(self) -> str:
return self._name
class Product:
name: str # Also satisfies the protocol
def __init__(self, name: str):
self.name = name
def greet(obj: Named) -> str:
return f"Hello, {obj.name}!"
greet(User("Alice")) # OK
greet(Product("Book")) # OKProtocol with Class Variables
from typing import Protocol, ClassVar
class Versioned(Protocol):
version: ClassVar[str]
class APIClient:
version: ClassVar[str] = "1.0.0"
class DatabaseClient:
version = "2.0.0" # Also works
def check_version(client: Versioned) -> str:
return client.versionCallable Protocol
from typing import Protocol
class Handler(Protocol):
def __call__(self, request: dict) -> dict: ...
def process(handler: Handler, request: dict) -> dict:
return handler(request)
# Function satisfies it
def my_handler(request: dict) -> dict:
return {"status": "ok", **request}
# Class with __call__ satisfies it
class MyHandler:
def __call__(self, request: dict) -> dict:
return {"status": "ok", **request}
process(my_handler, {}) # OK
process(MyHandler(), {}) # OKGeneric 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()) # 10Runtime Checkable Protocol
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
class File:
def close(self) -> None:
print("Closing file")
class Connection:
def close(self) -> None:
print("Closing connection")
# Can use isinstance at runtime
file = File()
print(isinstance(file, Closeable)) # True
# Without @runtime_checkable, isinstance raises TypeErrorCombining Protocols
from typing import Protocol
class Readable(Protocol):
def read(self) -> bytes: ...
class Writable(Protocol):
def write(self, data: bytes) -> None: ...
class ReadWritable(Readable, Writable, Protocol):
"""Combines both protocols."""
pass
# Or use intersection in type hints
def copy_data(src: Readable, dst: Writable) -> None:
data = src.read()
dst.write(data)Protocol for Iteration
from typing import Protocol, Iterator, TypeVar
T = TypeVar('T', covariant=True)
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):
return iter(range(self.start, self.end))
def sum_items(items: Iterable[int]) -> int:
return sum(items)
print(sum_items(NumberRange(1, 5))) # 10
print(sum_items([1, 2, 3, 4])) # 10Context 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 | None: ...
class Timer:
def __enter__(self) -> 'Timer':
import time
self.start = time.time()
return self
def __exit__(self, *args) -> None:
import time
self.elapsed = time.time() - self.start
def timed_operation(cm: ContextManager) -> None:
with cm:
pass # Do somethingSupports Pattern
from typing import Protocol
class SupportsAdd(Protocol):
def __add__(self, other: 'SupportsAdd') -> 'SupportsAdd': ...
class SupportsLessThan(Protocol):
def __lt__(self, other: 'SupportsLessThan') -> bool: ...
def add_items(a: SupportsAdd, b: SupportsAdd) -> SupportsAdd:
return a + b
def find_min(a: SupportsLessThan, b: SupportsLessThan) -> SupportsLessThan:
return a if a < b else b
# Works with any type that supports the operation
add_items(1, 2) # int
add_items("hello", " ") # str
find_min(1, 2) # int
find_min("a", "b") # strRepository Pattern with Protocol
from typing import Protocol, TypeVar, Generic, List, Optional
T = TypeVar('T')
ID = TypeVar('ID')
class Repository(Protocol[T, ID]):
def get(self, id: ID) -> Optional[T]: ...
def save(self, entity: T) -> T: ...
def delete(self, id: ID) -> bool: ...
def find_all(self) -> List[T]: ...
# Any class implementing these methods works
class InMemoryRepo:
def __init__(self):
self._data = {}
self._id = 0
def get(self, id: int):
return self._data.get(id)
def save(self, entity):
self._id += 1
self._data[self._id] = entity
return entity
def delete(self, id: int) -> bool:
if id in self._data:
del self._data[id]
return True
return False
def find_all(self):
return list(self._data.values())Event Handler Protocol
from typing import Protocol, TypeVar
E = TypeVar('E', contravariant=True)
class EventHandler(Protocol[E]):
def handle(self, event: E) -> None: ...
class ClickEvent:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class ClickHandler:
def handle(self, event: ClickEvent) -> None:
print(f"Clicked at ({event.x}, {event.y})")
def dispatch(handler: EventHandler[ClickEvent], event: ClickEvent):
handler.handle(event)
dispatch(ClickHandler(), ClickEvent(10, 20))When to Use Protocol vs ABC
| Use Protocol | Use ABC |
|---|---|
| Third-party types | Your own hierarchy |
| Structural typing | Nominal typing |
| No runtime checks needed | Need isinstance() |
| Flexible interfaces | Strict contracts |
| Duck typing | Explicit inheritance |
Protocols bring type safety to duck typing. Use them when you want interface guarantees without forcing inheritance.
React to this post: