Protocol vs ABC for Interfaces in Python

TL;DR

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 Protocol matching vs nominal ABC inheritance A FileSink class with a write method matches SupportsWrite structurally with no inheritance, but only counts as an AbstractSink if it explicitly inherits. class FileSink: def write(self, s): ... SupportsWrite(Protocol) matches by shape no inheritance needed AbstractSink(ABC) matches only if class FileSink(AbstractSink) automatic requires subclassing
A Protocol matches on shape; an ABC matches only through explicit inheritance.

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 vs static analysis A @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 isinstance on a bare Protocol: without @runtime_checkable you get TypeError: 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 raises TypeError. Implement every @abstractmethod in the concrete subclass.
  • Expecting @runtime_checkable to verify signatures: it checks name presence only; a wrongly-typed attribute still passes isinstance. 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.

Back to Protocol and Structural Subtyping