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. Protocol arrived in Python 3.8 via PEP 544; on 3.7 and earlier you import it from typing_extensions instead, which back-ports the identical behaviour so the type checker sees the same structural relationship.
# 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. Pyright reports the same rejection under reportArgumentType and, in strict mode, spells out exactly which member failed and why (for example "write" is incompatible: return type "None" is not assignable to "int").
The matching rules are stricter than “has an attribute of that name.” A method must be compatible in the sense of function subtyping: the parameter types are checked contravariantly and the return type covariantly, so a write that narrowed its parameter to bytes would not satisfy a Protocol asking for str. Attributes declared in a Protocol are treated as read-write by default, which makes them invariant — a subtype that offers a narrower attribute type will be rejected unless you mark the member read-only. You get a read-only attribute by declaring it as a @property (or by combining Final), which is the usual way to express “the object must expose a name, but callers only read it”:
from typing import Protocol
class Named(Protocol):
@property
def name(self) -> str: ... # read-only: covariant, easier to satisfy
class User:
name: str = "ada" # a plain str attribute satisfies the property
def greet(x: Named) -> str:
return f"hi {x.name}"
Because conformance is structural, a class can satisfy several unrelated Protocols at once without any of them appearing in its bases, and the standard library leans on exactly this: collections.abc.Iterable, Sequence, and friends double as both runtime ABCs and structural bases, so an object with __iter__ is accepted as Iterable[T] whether or not it inherits from anything. One thing structural matching does not give you is shared behaviour: if your Protocol declares a default method body, a conforming class does not automatically receive it. To inherit a default implementation you must subclass the Protocol explicitly — a Protocol can be used as a concrete base class, and only then does the class actually get the method. See Typing Collections and collections.abc for the standard-library duck types that are already Protocols.
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. abc.ABC is a convenience base whose metaclass is abc.ABCMeta; inheriting from ABC is equivalent to writing class AbstractSink(metaclass=ABCMeta). That metaclass is what refuses to build an instance while any @abstractmethod remains unimplemented.
# 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. Note the division of labour: the static [abstract] error is emitted by the type checker before you run anything, while the TypeError: Can't instantiate abstract class ... with abstract method write is raised by ABCMeta.__call__ at runtime even if you never ran a type checker. The two backstops are independent, which is a large part of an ABC’s appeal.
ABCs give you three tools a Protocol has no equivalent for. First, shared concrete methods like write_line above: every subclass inherits real behaviour, not just a shape. Second, __init_subclass__, an ordinary hook (not typing-specific, added in Python 3.6) that runs whenever a subclass is created — handy for registering plugins or validating that a subclass set a required class attribute:
from abc import ABC, abstractmethod
class Plugin(ABC):
registry: dict[str, type["Plugin"]] = {}
def __init_subclass__(cls, *, name: str, **kw: object) -> None:
super().__init_subclass__(**kw)
Plugin.registry[name] = cls # runs at class-creation time
@abstractmethod
def run(self) -> None: ...
class Echo(Plugin, name="echo"):
def run(self) -> None: ...
Third, virtual subclasses via ABC.register(). Calling AbstractSink.register(SomeClass) makes issubclass(SomeClass, AbstractSink) and isinstance return True at runtime without any inheritance — this is how collections.abc.Sequence claims list and tuple. The important caveat for typing: mypy and pyright do not honour register() statically. A registered class is a runtime subtype only; the type checker still expects real inheritance, so register() buys you runtime isinstance behaviour but no static guarantee. For a wider tour of the standard collections.abc hierarchy, see Typing Collections and collections.abc.
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.
Two further limits are worth committing to memory. First, @runtime_checkable is available for isinstance on any Protocol, but issubclass is only allowed against a non-data Protocol — one whose members are all methods. The moment a Protocol declares a plain data attribute, issubclass(cls, Proto) raises TypeError: Protocols with non-method members don't support issubclass(), because there is no reliable way to test an attribute’s presence on a class object rather than an instance. Second, the presence check really is just hasattr: it walks the required member names and confirms each exists, so a class that stores a non-callable under a method’s name, or that computes the attribute lazily in __getattr__, will still pass. That is why the Broken object below is reported as an instance despite being unusable.
@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.
There is also a real runtime cost to be aware of on older interpreters: before Python 3.12, isinstance against a @runtime_checkable Protocol re-computed the member set on every call and could be markedly slow in hot paths; Python 3.12 rewrote the check to be substantially faster and to cache the members. If you are on 3.8–3.11 and calling such checks in a loop, hoist the result or prefer an ABC. runtime_checkable itself is importable from typing since 3.8 and from typing_extensions for earlier versions.
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.
A useful tie-breaker is who writes the implementers. If the conforming classes live in code you do not control — plugins, third-party adapters, standard-library objects — a Protocol is the only option that does not force those authors to import and inherit your base. If every implementer is yours and you want to hand them constructor logic, invariants enforced in __init__, or a write_line-style default, an ABC pays for its extra weight. A common mature design uses both layers: publish a Protocol as the contract callers depend on, and ship an ABC as one convenient starter implementation of that contract. Callers stay decoupled from the hierarchy while your own subclasses get the shared behaviour. Note also that a Protocol can itself be generic — class SupportsWrite[T](Protocol) on 3.12+ (PEP 695) — so choosing structural typing does not cost you parametrisation.
Common mistakes
Most Protocol-versus-ABC bugs come from expecting one mechanism to behave like the other — asking a Protocol for runtime guarantees, or asking an ABC to match on shape. The pairs below show the wrong instinct against the correct one.
- 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]. Add the decorator, and remember it enablesisinstancebut not alwaysissubclass(data-member Protocols rejectissubclass). - Instantiating an ABC with a missing method: mypy reports
[abstract]and Python raisesTypeError. Implement every@abstractmethodin the concrete subclass. An abstract property or an abstractclassmethod/staticmethodcounts too — stack@abstractmethodinnermost (closest todef) or the method will not be registered as abstract. - Expecting
@runtime_checkableto verify signatures: it checks name presence only; a wrongly-typed attribute still passesisinstance. Keep the static check as your real gate, and treat the runtime check as a coarse “has these attributes” filter, not a validator. - 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 (Cannot instantiate protocol class "SupportsWrite"), and CPython raisesTypeErrorat runtime for the same reason. - Assuming
ABC.register()gives a static subtype: it only affects runtimeisinstance/issubclass. Type checkers ignore it, so a registered class passed where the ABC is expected still needs real inheritance to satisfy mypy or pyright. Useregister()for runtime interop, inheritance for static guarantees. For the neighbouring case of variance surprises, see mypy vs pyright on Protocol Variance.
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.