Protocols
Protocols
Section titled βProtocolsβWhat it is
Section titled βWhat it isβtyping.Protocol (Python 3.8+) enables structural subtyping (βduck typing,β but statically checkable): a class satisfies a Protocol by having the right methods/attributes, with no explicit inheritance required. This is different from abc.ABC, where a class must explicitly subclass and register.
Before this feature
Section titled βBefore this featureβTo type-check βanything with a .quack() method,β you had two bad options: use Any (no checking at all), or force every compatible class to explicitly inherit from a common base:
from abc import ABC, abstractmethod
class Quacker(ABC): @abstractmethod def quack(self): ...
class Duck(Quacker): # must explicitly inherit def quack(self): return "Quack!"
class Robot: # forgot to inherit β or it's a third-party class def quack(self): # has the right shape, but isn't a Quacker return "Beep-quack"
def make_it_quack(q: Quacker) -> str: return q.quack()
make_it_quack(Robot()) # type checker complains: Robot is not a Quacker # even though it works perfectly at runtimeAfter this feature
Section titled βAfter this featureβfrom typing import Protocol
class Quacker(Protocol): def quack(self) -> str: ...
class Duck: # no inheritance from Quacker at all def quack(self) -> str: return "Quack!"
class Robot: # also no inheritance def quack(self) -> str: return "Beep-quack"
def make_it_quack(q: Quacker) -> str: return q.quack()
make_it_quack(Duck()) # type-checks fine β Duck has a matching quack()make_it_quack(Robot()) # type-checks fine too β Robot has a matching quack()Why this is better
Section titled βWhy this is betterβ- No forced inheritance: third-party classes you donβt control, or classes that already inherit from something else, can still satisfy your interface just by having the right shape.
- Matches how Python already works at runtime: duck typing was always valid Python;
Protocollets static type checkers (mypy, pyright) actually verify it instead of forcingAny. - Retroactive compatibility: a
Protocolcan be satisfied by classes written before the protocol even existed, as long as their method signatures match.
Key notes / edge cases
Section titled βKey notes / edge casesβProtocolis purely a static-typing construct β it has zero effect at runtime by default;isinstance()checks against a plainProtocoldonβt work unless itβs decorated with@runtime_checkable.@runtime_checkablemakesisinstance()work, but it only checks that the methods exist (by name), not that their signatures match β runtime protocol checks are shallower than what the static type checker verifies.- A class satisfies a
Protocolimplicitly β thereβs noclass Duck(Quacker)needed, which is the entire point (structural, not nominal, typing).
from typing import Protocol, runtime_checkable
@runtime_checkableclass Sized(Protocol): def __len__(self) -> int: ...
print(isinstance([1, 2, 3], Sized)) # True -- list has __len__print(isinstance(42, Sized)) # False -- int has no __len__Quick practice
Section titled βQuick practiceβ-
Does a class need to inherit from a
Protocolto satisfy it?Answer
No β it just needs matching method/attribute signatures. That's the "structural" in structural subtyping. -
Whatβs needed to make
isinstance()work against aProtocol?Answer
Decorate the protocol with `@runtime_checkable` β plain `Protocol` classes don't support `isinstance()` checks by default. -
How is
Protocoldifferent from subclassingabc.ABC?Answer
`ABC` requires explicit inheritance (nominal typing) to be recognized as implementing the interface; `Protocol` only requires having the right methods, with no inheritance relationship at all (structural typing).