skip to content

How does typing.Protocol differ from abc.ABC when you define an interface?

level: middleimportance: must knowfreq 50%

answer

  1. Nominal versus structural conformance
  2. One needs inheritance, one needs matching members
  3. One is enforced by the interpreter
  4. One is checked only by a static tool
  5. Base classes can also ship shared code

basics

~20 s

An abc.ABC is nominal: a class conforms only by inheriting from it, and the interpreter enforces that at instantiation. A typing.Protocol is structural: any class with matching members conforms, checked by a static type checker with no inheritance and no runtime cost.

solid answer

~40 s

`abc.ABC` gives you a **nominal** interface. The implementer imports your base class, inherits from it, and the interpreter refuses to instantiate the subclass while an `abc.abstractmethod` is unimplemented, raising `TypeError`. You also get to ship concrete helper methods on that base, and `isinstance` works for free. `typing.Protocol` (Python 3.8, PEP 544) gives you a **structural** interface: a class conforms if it has members with compatible signatures, and a static type checker decides that by comparison — no import, no inheritance, nothing at runtime. That makes Protocols the only option for classes you do not own, including standard-library types, and it lets the *consumer* declare what it needs. The prices are symmetric: an ABC forces every implementer to depend on you; a Protocol is invisible unless someone actually runs a type checker.

code

python · 13 lines
python
from typing import Protocol

class Sink(Protocol):
    def write(self, line: str) -> None: ...

class PrintSink:            # does not inherit from Sink
    def write(self, line: str) -> None:
        print(line)

def emit(sink: Sink, line: str) -> None:
    sink.write(line)

emit(PrintSink(), "cpu=0.42")

go deeper

for a junior

Learn the one-line contrast: an abstract base class must be inherited and the interpreter enforces it, while a Protocol is satisfied by having the right methods and is checked by a tool, not by Python.

for a middle

Explain the mechanics both ways — abstractmethod plus the TypeError at instantiation, implicit structural matching by a type checker, and the fact that a Protocol adds nothing to the runtime object at all.

for a senior

Argue the choice from constraints: do you need runtime isinstance dispatch, do implementers exist already, do you have a type-checking gate in CI, and do you want to ship shared implementation alongside the contract.

for a principal

Own what publishing each one commits you to — an abstract base class is a hard dependency and an inheritance contract you must evolve carefully, a Protocol is a contract only as real as the tooling that enforces it.

The two mechanisms answer the same question — "what does this argument have to support?" — with opposite theories of what makes an object acceptable. **`abc.ABC` is nominal typing.** `abc.ABC` is a plain class whose metaclass is `abc.ABCMeta`; subclassing it (or setting the metaclass directly) opts the class into abstract-method machinery. Methods marked with `abc.abstractmethod` are recorded when the class body is executed, and any attempt to instantiate a class that still leaves one unimplemented raises `TypeError: Can't instantiate abstract class ... with abstract method ...`. Membership is by declaration: `FileSink` is a `Sink` because it says `class FileSink(Sink)`. That buys you three things a Protocol cannot give. First, **runtime enforcement** — the mistake is caught at construction, with no external tool involved. Second, **shared implementation**: the abstract base is a real base class, so it can carry concrete methods built on top of the abstract ones (the classic template-method shape) and mixin behaviour. Third, **`isinstance` and `issubclass` work immediately**, which matters for registries, plugin dispatch and runtime branching. `abc.ABCMeta` also offers an escape hatch for classes you do not own: `abc.ABCMeta.register` makes an unrelated class a *virtual* subclass, so `isinstance` and `issubclass` report `True`. It is worth knowing precisely what that does and does not do: it verifies nothing, it does not put the ABC into the class's method resolution order, and the registered class inherits none of the ABC's concrete methods. It is a runtime assertion by the programmer, not a check. **`typing.Protocol` is structural typing.** You declare a class inheriting `Protocol` whose members describe the required shape. Any class with compatible members satisfies it — implicitly, with no inheritance and no import of your module. Nothing happens at runtime at all: a Protocol used as an annotation is metadata, and a static type checker does the comparison. Consequences: - **It works on code you do not own.** Standard-library types and third-party classes satisfy your Protocol without their author knowing it exists, which is impossible with an ABC unless you reach for `register`. - **The consumer can define the interface.** The module that *needs* a `write` method declares the Protocol, so the dependency arrow points from the provider's shape to the consumer's requirement instead of forcing every provider to import a shared base. - **It costs nothing at runtime** — no metaclass, no extra base in the MRO, no per-instantiation check. - **A plain Protocol is not usable with `isinstance`**; that requires the `typing.runtime_checkable` decorator, with its own semantics. **Where the sharp edges are.** A Protocol checks *shape*, not behaviour: a class whose `write` accepts a `str` and silently drops half of it conforms perfectly. And because the check is external, a Protocol you publish is only as real as the CI job that runs the type checker; without one it is an elaborate comment. On the ABC side, the enforcement is real but coarse — it fires at instantiation, not at definition, and it says nothing about signatures: an override with an incompatible parameter list satisfies the abstract-method machinery just fine. **They compose.** A class *may* explicitly inherit from a Protocol, which makes a type checker verify conformance at the definition site — useful when you want the implementer to be told rather than the caller. Be aware of what that inherits: the Protocol's method bodies are usually `...`, so an inherited-but-unoverridden member is a real method that returns `None` rather than an error, unless the Protocol also derives from `abc.ABC` and marks members abstract. A common library shape is to publish both: a Protocol as the typed contract for callers, and an optional abstract base carrying convenience methods for implementers who want them. The standard library does something similar in `collections.abc`, where the abstract bases both supply mixin methods and answer structural `isinstance` questions through `__subclasshook__`. **Choosing.** Reach for an ABC when you own both sides and want shared code, runtime enforcement or `isinstance` dispatch. Reach for a Protocol when the implementer should not have to depend on you, when the classes already exist, or when each consumer wants a narrow contract describing only what it actually calls.

  • What does `abc.ABCMeta.register` actually change about a class?
    It records the class as a virtual subclass, so `isinstance` and `issubclass` answer `True`. That is all. Nothing is verified — a class missing every abstract method registers happily — the abstract base does not enter the registered class's method resolution order, and none of its concrete methods are inherited. It is a runtime assertion you are making, useful for adapting classes you do not own to an existing isinstance-based dispatch.
  • If a class explicitly inherits from a Protocol, what changes?
    A static type checker then verifies conformance at the class definition rather than at each call site, so the implementer sees the error. At runtime it becomes an ordinary base class, which means any member you fail to override is inherited with its `...` body and quietly returns `None`. If you want that to fail loudly, the protocol members need to be abstract as well.
  • Can a Protocol require an attribute rather than a method?
    Yes — a bare annotation in the protocol body, such as `name: str`, requires the attribute, and an implementer can satisfy it with an instance attribute, a class attribute or a property. This is one place structural typing is more expressive than an abstract method, which can only describe something callable unless you layer a property on top.

saying these in an interview costs you the question

  • Says a class must inherit from a Protocol to conform
  • Claims Protocols are checked by the interpreter at runtime
  • Thinks ABCMeta.register verifies the methods exist
  • Believes an ABC checks override signatures
  • Says Protocols can be used with isinstance by default
  • Treats structural conformance as a behavioural guarantee

context