Does a class need to inherit from a typing.Protocol for a type checker to accept it?
answer
- Shape, not ancestry
- The implementer imports nothing
- The consumer declares what it needs
- Members matched by name and signature
- Verified statically at the use site
basics
~10 sNo. typing.Protocol is structural: a static type checker accepts any class whose members match the protocol's declared methods and attributes, with no base class, no registration and no import on the implementing side.
solid answer
~50 s`typing.Protocol` declares a **shape**, not an ancestry. A class conforms the moment its methods and attributes match the protocol's declared members by name and compatible signature; it never imports the protocol, subclasses it, or registers with it. That inverts ownership of the interface: the consumer writes the protocol describing exactly what it needs, and existing classes -- including ones from a third-party library you cannot edit -- satisfy it retroactively. Conformance is verified by a static checker at every place a value flows into a protocol-annotated parameter, variable or return; at runtime nothing is enforced by default, and `isinstance` against a plain protocol is rejected outright. You may still list a protocol as a base class explicitly, which asks the checker to verify the implementation at its own definition rather than at each call site.
code
python · 22 linesfrom typing import Protocol, runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
@runtime_checkable
class ClosableRT(Protocol):
def close(self) -> None: ...
class Handle:
def close(self) -> None: ...
try:
isinstance(Handle(), Closable)
except TypeError as exc:
print("TypeError:", exc)
print(isinstance(Handle(), ClosableRT))go deeper
Be ready to state the one-line rule: a protocol describes required methods and attributes, and any class with that shape is accepted without inheriting it. Know that annotations are checked by a tool, not by the interpreter.
Explain that matching compares signatures and not just names, that extra members are harmless, and that the error surfaces at the call site unless you subclass the protocol explicitly to move the check to the definition.
Show the design payoff: consumer-defined, deliberately narrow ports that let unowned third-party classes conform retroactively, and know the runtime edges -- protocols cannot be instantiated and cannot inherit non-protocol bases.
Own the tradeoff of losing a single enumerable list of implementations. Decide when a codebase should standardise on narrow structural ports at module boundaries versus a declared hierarchy people can discover by reading one file.
## The claim `Protocol` makes `typing.Protocol` is Python's way of writing an interface that is satisfied by **shape** rather than by **declaration**. A protocol class body lists members -- methods, and data attributes as annotations -- and a static type checker treats any class whose own members are compatible with that list as an acceptable value wherever the protocol is annotated. The implementing class does not import the protocol, does not name it as a base class, and does not register itself anywhere. This is what "structural" means, in contrast with the *nominal* typing that most class hierarchies use, where a value is accepted because of what it inherits from. ```python from typing import Protocol class Renderer(Protocol): def render(self, doc: str) -> bytes: ... class PdfRenderer: # imports nothing, inherits nothing def render(self, doc: str) -> bytes: return doc.encode() def convert(renderer: Renderer, doc: str) -> int: return len(renderer.render(doc)) convert(PdfRenderer(), "report") # accepted by the checker ``` ## Who owns the interface The practical consequence is that the interface moves to the **consumer**. In a nominal design, the provider of a class must have anticipated your abstraction and inherited from it; if it did not, you are stuck writing an adapter. With a protocol, the function that needs `render` declares a one-method protocol next to itself, and every class that already has a compatible `render` -- yours, a colleague's, one from a package you cannot modify -- conforms retroactively and silently. The protocol can therefore be deliberately narrow: it should list the members the consumer actually calls, not everything a plausible implementation might offer. A one-method protocol is a normal and good protocol. ## What "matching" means Matching is more than matching names. The checker compares signatures: parameter kinds and names, defaults, and the return type must be compatible. A method whose parameter is annotated `str` in the protocol may be implemented with a wider parameter type (`object` or `str | bytes`) because parameters are contravariant, and may return a narrower type than declared because returns are covariant. Extra members on the implementing class are irrelevant -- structural conformance asks "does it have at least this?", never "does it have exactly this?". Missing members, mismatched arity, or an incompatible return type produce an error, and crucially that error is reported **at the use site**: the line that passes the object into the protocol-annotated parameter, which is often nowhere near the class definition. ## Runtime does nothing by default None of this is enforced when the program runs. Annotations are not checks; a class that does not conform is still passed happily and fails later with `AttributeError` or `TypeError` if the missing member is reached. `isinstance(x, Renderer)` against a plain protocol does not fall back to a structural test -- it raises, because instance checks require an opt-in decorator. Two runtime rules do bite at class-creation time, though. A protocol class cannot be instantiated: `Renderer()` raises `TypeError: Protocols cannot be instantiated`, because a protocol is a specification, not a thing. And a protocol may only inherit from other protocols; mixing a non-protocol base into a `Protocol` subclass raises `TypeError: Protocols can only inherit from other protocols`. The reverse is fine and often useful: a concrete class may list a protocol among its bases. ## Explicit subclassing as an option, not a requirement Naming the protocol as a base class -- `class PdfRenderer(Renderer):` -- is legal and changes two things. The checker verifies the implementation once, at the class definition, so a missing or misspelled method is reported there instead of at every call site; and the subclass inherits any member of the protocol that was given a real body, which serves as a default implementation. What it does *not* do is change who else conforms: unrelated classes with the right shape are still accepted, because the protocol has not stopped being structural. Note that a protocol member written as `...` is not abstract at runtime unless it is also decorated with `abc.abstractmethod`, so a subclass that forgets to implement it will instantiate happily and return `None` from that member -- the checker is what catches this, not the interpreter. ## When to reach for it Protocols pay off when the dependency you are describing is small and the implementations are many or outside your control: a callback shape, a "thing with `read`", a narrow port at a module boundary that you want to substitute in tests without inheriting anything. They cost you the ability to point at a single class as the definitive list of implementations, which is exactly the trade a structural interface makes.
- If nothing inherits the protocol, where does a checker actually report a conformance error?At the use site. The check happens wherever a value flows into a protocol-annotated parameter, variable, attribute or return, so the error lands on the call that passes the object -- possibly in a different module from the class. Listing the protocol as an explicit base class moves the check to the class definition instead, which is the usual reason to do it.
- What happens if you instantiate a class that directly subclasses typing.Protocol?It raises `TypeError: Protocols cannot be instantiated`. A protocol is a specification, so only concrete implementations are instantiated. A concrete class that lists the protocol among its bases is a normal class and instantiates fine, inheriting any protocol member that was given a real body as a default implementation.
- Can a Protocol class inherit from a regular non-protocol class?No. Creating the class raises `TypeError: Protocols can only inherit from other protocols`. Protocols compose with protocols -- a larger protocol can inherit two smaller ones -- but cannot mix in concrete bases. The opposite direction is allowed: a concrete implementation may list a protocol among its base classes.
A nominal base class is a club membership card you must apply for; a protocol is a door with a height mark on it -- anyone tall enough walks through, whether or not they ever heard of the door.
saying these in an interview costs you the question
- Says the implementing class must subclass the Protocol
- Claims Python enforces protocol conformance at runtime by default
- Thinks a class must be registered with the protocol first
- Believes only classes you own can ever conform
- Says matching method names is enough and signatures do not matter
- Thinks a protocol class can be instantiated like a normal class