Skip to content

← Structural Typing and the Hard Parts step 6 of 24

Medium Primitives

Subclassing a Protocol: what Python will and will not stop you doing

“I inherited from the Protocol so Python will stop me forgetting a method.” It will not.

Protocol members are not @abstractmethod by default. A class that explicitly subclasses a protocol and implements none of it is a mypy [abstract] error at the point of instantiation — and constructs perfectly happily at runtime. __abstractmethods__ on that subclass is empty. Ship a plugin like that and it fails with an AttributeError deep inside a request, not with a TypeError at construction.

Explicit subclassing is not useless, though: it is the only way to inherit a protocol’s default method bodies. An implicit implementer that wants the same behaviour has to copy it.

Your task

Given a protocol Runner with a required run(payload) -> str and a default run_twice(payload) that returns self.run(self.run(payload)), produce four classes:

  • Implicit — does not subclass Runner. run upper-cases. It must reimplement run_twice itself: the protocol declares that member, so a structural implementer needs it too, and gets no help writing it.
  • Explicit(Runner)run appends ".", and run_twice is inherited.
  • Missing(Runner) — declares nothing at all.
  • AbcMissing(AbcRunner) — the ABC counterpart, for contrast.
def solve(payload: str) -> dict[str, object]:

builds runners: list[Runner] = [Implicit(), Explicit()] — proving the structural and the nominal implementer are interchangeable at the type level — and returns:

key value
"outputs" [r.run_twice(payload) for r in runners]
"explicit_inherits_default" Explicit.run_twice is Runner.run_twice
"implicit_inherits_default" Implicit.run_twice is Runner.run_twice
"protocol_subclass_abstracts" sorted(Missing.__abstractmethods__)
"abc_subclass_abstracts" sorted(AbcMissing.__abstractmethods__)
"protocol_subclass_build" try_build(Missing)
"abc_subclass_build" try_build(AbcMissing)

try_build(cls: type) -> str returns "TypeError" if cls() raises TypeError, else "constructed".

Read the result carefully

Missing.__abstractmethods__ comes back empty and try_build(Missing) returns "constructed". AbcMissing comes back with ["run"] and refuses to build. Same omission, two completely different runtime guarantees — and only one of them is the one people assume they are getting.

The identity checks are the other half: is comparison of the two function objects shows, concretely, that the default body is shared with the protocol in the explicit case and merely duplicated in the implicit one.

Practical rule

If you need construction-time enforcement, use an ABC. If you need to describe a shape you do not own, use a Protocol. Publishing both — the Protocol for callers, the ABC as an internal convenience base — is a legitimate and common design.