Skip to content

Protocols

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.

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 runtime
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()
  • 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; Protocol lets static type checkers (mypy, pyright) actually verify it instead of forcing Any.
  • Retroactive compatibility: a Protocol can be satisfied by classes written before the protocol even existed, as long as their method signatures match.
  • Protocol is purely a static-typing construct β€” it has zero effect at runtime by default; isinstance() checks against a plain Protocol don’t work unless it’s decorated with @runtime_checkable.
  • @runtime_checkable makes isinstance() 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 Protocol implicitly β€” there’s no class Duck(Quacker) needed, which is the entire point (structural, not nominal, typing).
from typing import Protocol, runtime_checkable
@runtime_checkable
class 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__
  1. Does a class need to inherit from a Protocol to satisfy it?

    AnswerNo β€” it just needs matching method/attribute signatures. That's the "structural" in structural subtyping.
  2. What’s needed to make isinstance() work against a Protocol?

    AnswerDecorate the protocol with `@runtime_checkable` β€” plain `Protocol` classes don't support `isinstance()` checks by default.
  3. How is Protocol different from subclassing abc.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).