Structural Typing with Protocol
`typing.Protocol` adds static structural typing (duck typing) to Python: any object that has the right methods is acceptable, no inheritance required. It is the right tool for plugin interfaces, dependency injection, and 'looks like a file' style APIs. This entry covers a basic protocol, the `@runtime_checkable` switch, and how Protocol differs from an abstract base class.
1,131 views
25
from typing import Protocol
class SupportsArea(Protocol):
"""Anything with a no-arg `.area()` returning a float."""
def area(self) -> float: ...
class Circle:
def __init__(self, r: float):
self.r = r
def area(self) -> float:
return 3.14159 * self.r * self.r
class Rect:
def __init__(self, w: float, h: float):
self.w, self.h = w, h
def area(self) -> float:
return self.w * self.h
def total_area(shapes: list[SupportsArea]) -> float:
return sum(s.area() for s in shapes)
# Neither Circle nor Rect inherits from SupportsArea, but both satisfy it.
shapes = [Circle(1.0), Rect(2.0, 3.0), Circle(2.0)]
print(round(total_area(shapes), 2)) # 22.71Protocol is the static type for 'has these methods', the way Python developers always meant duck typing. Circle and Rect are not subclasses of SupportsArea, but the type checker treats them as compatible because they have an area() method with the matching signature. There is zero runtime cost: the protocol exists only for the type checker. Use protocols for plugin interfaces, anything you want to accept user-defined classes for, or any function that previously documented its expectations in a docstring like 'must have a .read() method'.
from typing import Protocol, runtime_checkable
@runtime_checkable
class HasClose(Protocol):
def close(self) -> None: ...
class Connection:
def close(self) -> None:
print('connection closed')
class File:
def close(self) -> None:
print('file closed')
class Plain:
pass
for obj in (Connection(), File(), Plain()):
if isinstance(obj, HasClose):
obj.close()
else:
print(f'{type(obj).__name__} has no .close()')
# connection closed
# file closed
# Plain has no .close()Plain Protocols exist only at type-check time; isinstance(x, MyProtocol) raises a TypeError. Decorating with @runtime_checkable makes isinstance work by checking attribute presence at runtime. The check is shallow: it only verifies the attribute exists, not its full signature, so isinstance(obj, HasClose) returns True for any object that happens to have a .close attribute (even a non-callable one). Use it for the 'do you support this duck behaviour?' branch in plugin loaders or context managers, but pair it with explicit calls so signature mismatches still fail loudly.
from typing import Protocol
from abc import ABC, abstractmethod
# Protocol: structural, opt-in by anyone, zero inheritance.
class Renderer(Protocol):
def render(self, frame: int) -> str: ...
# Abstract base class: nominal, opt-in by inheritance, can ship default code.
class BaseRenderer(ABC):
@abstractmethod
def render(self, frame: int) -> str: ...
def render_all(self, frames: range) -> list[str]:
return [self.render(f) for f in frames] # default behaviour
# Random user code that does not import Renderer at all.
class DotRenderer:
def render(self, frame: int) -> str:
return '.' * frame
# Protocol path: works without subclassing.
def draw(r: Renderer, n: int) -> str:
return r.render(n)
print(draw(DotRenderer(), 5)) # .....
# ABC path: must inherit, but you get the default render_all method for free.
class StarRenderer(BaseRenderer):
def render(self, frame: int) -> str:
return '*' * frame
sr = StarRenderer()
print(sr.render_all(range(1, 4))) # ['*', '**', '***']Pick Protocol when you want to type-check existing classes you do not own (third-party libraries, user plugins, mock objects in tests). Pick an abstract base class when you want to ship default method implementations alongside the abstract requirements, or when 'is-a' relationships matter for isinstance and registry lookups. The two are not mutually exclusive: it is common to declare a Protocol for the public contract and a separate ABC that implements it for users who want the convenience helpers. The mental shortcut: ABC asks 'are you my subclass?', Protocol asks 'do you have my methods?'.
