Python Protocols Separate Static Shape from Runtime Identity
How structural subtyping, runtime-checkable protocols, generic variance, and explicit implementations fit together.
An abstract base class establishes an explicit runtime relationship. A typing Protocol can instead describe the members a consumer needs, allowing static structural subtyping without requiring inheritance.
from typing import Protocol
class SupportsClose(Protocol):
def close(self) -> None: ...
def finish(resource: SupportsClose) -> None:
resource.close()
Any statically compatible class can be passed to finish, even if its author never imported the protocol. This is useful at library boundaries because the consumer owns the minimal interface.
Protocol bodies can provide defaults
Protocol methods may have implementations. A class that explicitly inherits the protocol can inherit those defaults, while a structurally compatible class that does not inherit it merely satisfies the declared shape.
Protocols can also declare data attributes. Writable attributes are invariant because a consumer may both read and assign them. A read-only @property can express a covariant observation more flexibly.
from typing import TypeVar, Protocol
T_co = TypeVar('T_co', covariant=True)
class Box(Protocol[T_co]):
@property
def value(self) -> T_co: ...
Variance must agree with how the type variable is used; type checkers reject a protocol declared covariant when it exposes an unsafe writable position.
Runtime checking is intentionally shallow
Adding @runtime_checkable permits isinstance(value, ProtocolName) and issubclass in supported cases.
from typing import runtime_checkable
@runtime_checkable
class Named(Protocol):
name: str
The runtime check validates both attribute presence and the annotated types and method signatures. isinstance(obj, Named) returns false when obj.name is an integer, and callable parameters are inspected for compatibility.
Parameterised checks retain their arguments
Because Python keeps generic arguments on typing objects, a parameterised runtime protocol can be used directly:
isinstance(candidate, Box[int])
The check verifies that the candidate’s value is an integer. This is a convenient runtime counterpart to static generic checking.
Explicit inheritance still has value
Structural compatibility keeps producers decoupled, but explicit inheritance can catch missing members while defining a class and can provide default implementations. It also signals intent to human readers.
Conversely, a large protocol can create accidental coupling. Prefer small capability-oriented protocols at parameters and richer concrete return types where callers need them. The goal is not to replace every base class, but to describe the narrow static evidence a consumer actually uses.