Yeda AI Tips · #127

Español

Protocol Over Inheritance: Structural Test Seams in Python

Duck typing with a type checker? Python's Protocol does both. Since Python 3.8, typing.Protocol gives you structural subtyping: any object with the right shape satisfies the type, and the checker verifies it — no shared base class, no abc.ABC, no import of your class into the test double.

The problem with inheritance seams

To swap a real dependency for a fake in tests, the classic move is a shared abstract base class. But that couples three things that shouldn't be coupled:

  1. The real implementation must inherit the base.
  2. The test fake must import and inherit the same base.
  3. Every new implementation — including ones in code you don't own — must opt in.

That coupling spreads. Third-party classes can't retroactively inherit your ABC, so you end up writing adapter wrappers whose only job is to say "yes, I'm one of those."

The structural fix

Define a Protocol with just the method you need. Any object that implements it satisfies the type — the class never mentions the protocol:

from typing import Protocol

class Renderer(Protocol):
    def render(self, template: str) -> str: ...

class HtmlRenderer:            # no base class
    def render(self, template: str) -> str:
        return f"<html>{template}</html>"

class FakeRenderer:            # test double: just implement the method
    def render(self, template: str) -> str:
        return "stub"

def publish(r: Renderer, tpl: str) -> str:
    return r.render(tpl)

publish(HtmlRenderer(), "x")   # type-checks
publish(FakeRenderer(), "x")   # type-checks too

The typing spec is explicit: a concrete type is assignable to a protocol if and only if it implements all of the protocol's members — explicit inheritance is allowed but "not necessary … for the sake of type-checking." Your fake stays a plain class in the test file, and mypy/pyright still catch a renamed method or a wrong return type.

Rules of thumb

SituationReach for
Test seam for one or two methodsProtocol — smallest interface wins
You control every implementation and want shared behaviorABC with concrete helper methods
Third-party classes must satisfy the interfaceProtocol (they can't inherit your ABC)
You need isinstance() at runtime@runtime_checkable Protocol — but see the caveat below
Attribute, not methodProtocols support data members: name: str in the body

Power-user notes

Resources

Watch the 60-second version: reel #127 in the Yeda AI Tips series. Building AI-assisted engineering workflows? Yeda AI designs, audits, and ships production LLM systems.

Talk to us · Read the blog · Leer en español