Protocol vs ABC for Interfaces in Python
typing.Protocol describes an interface structurally: any class whose methods and attributes match the shape satisfies it, with no inheritance and no registration. An abstract base class (abc.ABC) is nominal: a class only counts as a subtype if it explicitly inherits from it (or is .register()-ed). Reach for a Protocol to type duck-typed objects you do not own; reach for an ABC when you want enforced inheritance plus shared implementation and constructor guarantees.
Both a Protocol and an ABC let you say “this argument must support these methods.” The difference is how membership is decided. Choosing wrong tends to surface as either a [misc] “Cannot instantiate abstract class” error you did not expect, or an interface that silently fails to match a perfectly good implementation. This page is part of Protocol and structural subtyping and contrasts the two mechanisms directly.
Structural: a Protocol matches on shape
A Protocol declares the members an object must expose. Any class that provides them conforms, whether or not its author ever heard of your Protocol. That makes Protocols ideal for typing objects from third-party libraries or standard-library duck types.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
from typing import Protocol
class SupportsWrite(Protocol):
def write(self, data: str) -> int: ...
class FileSink: # no base class, no import of SupportsWrite
def write(self, data: str) -> int:
return len(data)
def emit(sink: SupportsWrite, line: str) -> None:
sink.write(line)
emit(FileSink(), "ok") # accepted structurally
If FileSink.write had the wrong signature — say it returned None — mypy would reject the call with [arg-type]: Argument 1 to "emit" has incompatible type "FileSink"; expected "SupportsWrite". The match is checked member by member, including method signatures and attribute types.
Nominal: an ABC demands inheritance
An abstract base class defines the same surface, but conformance is by name. A class is an AbstractSink only if it inherits from it. In exchange, the ABC can carry concrete helper methods and enforce that abstract methods are implemented before instantiation.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
from abc import ABC, abstractmethod
class AbstractSink(ABC):
@abstractmethod
def write(self, data: str) -> int: ...
def write_line(self, data: str) -> int: # shared implementation
return self.write(data + "\n")
class NetworkSink(AbstractSink):
def write(self, data: str) -> int:
return len(data)
NetworkSink() # fine — write is implemented
Forget to implement write and the class cannot be instantiated: mypy reports [abstract] (Cannot instantiate abstract class "NetworkSink" with abstract attribute "write") and CPython raises TypeError at runtime. That enforcement — plus the shared write_line — is exactly what a Protocol cannot give you.
isinstance and @runtime_checkable
A plain Protocol is a static-only construct; isinstance(obj, SupportsWrite) raises TypeError. Decorate it with @runtime_checkable to allow the check, but understand its limit: it verifies only that the named attributes exist, never their signatures or types.
# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsWrite(Protocol):
def write(self, data: str) -> int: ...
class Broken:
write = "not callable" # attribute exists, wrong kind
isinstance(Broken(), SupportsWrite) # True at runtime — only presence is checked
An ABC’s isinstance is reliable by comparison, because membership is recorded on the class. If you need trustworthy runtime checks, an ABC (or an explicit registration) is the sounder tool.
@runtime_checkable Protocol's isinstance check inspects attribute *names* only, so Broken above passes at runtime even though mypy would reject it structurally. The static checker verifies signatures; the runtime check does not. Never rely on a runtime Protocol check to validate method shapes.
Choosing between them
Prefer a Protocol when the interface is a lightweight capability (“anything with .write”), when the implementers are outside your control, or when you want to avoid coupling callers to a class hierarchy. Prefer an ABC when you own the hierarchy, want to ship shared behaviour, need constructor or invariant enforcement, or depend on reliable isinstance. The two are not exclusive — a Protocol can describe the public capability while an ABC provides one concrete family that satisfies it.
Common mistakes
- Calling
isinstanceon a bare Protocol: without@runtime_checkableyou getTypeError: Instance and class checks can only be used with @runtime_checkable protocols. mypy also flags the call site with[misc]. - Instantiating an ABC with a missing method: mypy reports
[abstract]and Python raisesTypeError. Implement every@abstractmethodin the concrete subclass. - Expecting
@runtime_checkableto verify signatures: it checks name presence only; a wrongly-typed attribute still passesisinstance. Keep the static check as your real gate. - Adding
__init__logic to a Protocol: Protocols are not meant to be instantiated or subclassed for behaviour; putting real constructor code there is an ABC’s job. mypy treats explicit Protocol instantiation as an error.
FAQ
Can a class satisfy a Protocol and an ABC at once? Yes. A class can inherit an ABC (making it a nominal subtype) while also structurally matching an unrelated Protocol. Static checkers evaluate each relationship independently, so one implementation can be passed to functions typed either way.
Is a Protocol slower or heavier than an ABC?
At runtime a Protocol usually costs nothing because the conformance check is erased; only @runtime_checkable isinstance calls do work. An ABC participates in the MRO and metaclass machinery, so it carries slightly more runtime weight — though rarely enough to matter.