Skip to content

← Orientation and the Gate step 9 of 13

Easy Primitives

Placement 2/6: Protocol vs ABC

Placement diagnostic, 2 of 6. About seven minutes. No worked example — if this one is opaque, T2 (Structural Typing and the Hard Parts) is where you start.


An ABC is nominal: Duck implements it because Duck inherits from it. A Protocol is structural: Duck implements it because Duck happens to have the right methods, whether or not Duck has ever heard of the protocol. That difference decides who is allowed to satisfy your interface. An ABC cannot be satisfied by a class you do not own — a third-party type, a stdlib type, a mock. A Protocol can.

The module gives you three unrelated classes and a registry mapping names to them. Duck and Robot both have speak() -> str. Rock has roll() and no speak. None of them inherits from anything.

Write:

def solve(order: list[str]) -> list[str]:

For each name in order, in order:

  • unknown name → "<unknown>"
  • the constructed object satisfies Speaker → the result of speak()
  • otherwise → "<silent>"

Names are case-sensitive: "Duck" is not "duck".

Three things this measures

@runtime_checkable. isinstance(obj, SomeProtocol) raises TypeError at runtime unless the protocol carries that decorator, and mypy separately reports Only @runtime_checkable protocols can be used with instance and class checks [misc]. The starter deliberately omits it.

What isinstance on a protocol actually checks. Presence of the named attributes, and nothing else. Not signatures, not parameter types, not return types. A class with speak = 42 passes isinstance(obj, Speaker) while obj.speak() raises TypeError. @runtime_checkable is a cheap smoke test, not interface validation — the real validation is the static check mypy performs at every call site.

Why not hasattr. if hasattr(obj, "speak"): obj.speak() also runs, and also type-checks — because mypy narrows a hasattr to give the attribute type Any. obj.speak(1, 2, 3, "garbage") type-checks too. You have written code that looks defensive and has silently opted out of checking. The Protocol narrows object to Speaker, so obj.speak() is known to return str and a wrong call is a compile error.

Production consequence

This is the seam question. If your Storage interface is an ABC, every test double must subclass it and every third-party adapter needs a wrapper. If it is a Protocol, unittest.mock.MagicMock, an in-memory dict, and the real S3 client can all satisfy it without a single import pointing from the implementations back at your interface — which is the actual definition of a dependency inversion.

Loading visualization…