Skip to content

← Structural Typing and the Hard Parts step 1 of 24

Easy Primitives

Protocols: what structural subtyping actually asserts

A function that demands a nominal base class is viral. Every caller must either subclass your type or write an adapter — including callers holding a stdlib object, a third-party client, or a test double they do not own. That is how a two-line helper ends up dictating the inheritance graph of a codebase, and it is the single most common reason a Python interface is rejected in review.

typing.Protocol inverts the requirement. A class satisfies a protocol by having compatible members; the protocol need not appear anywhere in the class’s MRO, and the implementing class need not import it. The protocol is a description of a shape, not a membership card.

Your task

Define a protocol SupportsSizeBytes declaring exactly one member — size_bytes(self) -> int — and a function

def total_size(items: Iterable[SupportsSizeBytes]) -> int:

that sums it over any iterable. Then implement three completely unrelated classes, none of which mentions the protocol:

  • Blob — a frozen dataclass with an nbytes: int field; its size is nbytes.
  • TextFile — a plain class holding text: str; its size is the length of the UTF-8 encoding, not the length of the string. (len("日本語") is 3; its size on the wire is 9. Getting this wrong is a real bug, not a contrived one.)
  • Packet — a NamedTuple with words: int; its size is 4 * words + 8.

Finally:

def solve(blob_sizes: list[int], texts: list[str], packet_words: list[int]) -> int:

builds one Blob per entry of blob_sizes, one TextFile per entry of texts, one Packet per entry of packet_words, puts them all in one list, and returns total_size of it.

Where the type system does the work

A heterogeneous list has no useful inferred type — mypy will land on something like list[object], and total_size will reject it. The list must be declared:

items: list[SupportsSizeBytes] = []

That single annotation is the whole lesson. It is the only place the three classes are asserted to share a shape, and it is checked structurally at that point, not at each class definition.

Two rules that bite later

A protocol’s other bases must all themselves be protocols. And if you subclass a protocol while forgetting to list Protocol among the bases, you do not get a protocol — you silently get an ordinary ABC, and every implicit implementer stops type-checking with an error that points at the call site rather than at your class.

Your submission must pass mypy --strict.

Loading visualization…