Skip to content
← All Python articles

Python explainer

Choose Between a Protocol and an ABC

About 4 min to read. Allow extra time to work through examples.

Use a protocol to say which methods a function needs, and an ABC to share code with subclasses and make them define the rest. Often you want both.

This article uses Python. You can make it your practice mode to browse related content across the site.

You are writing exchange(codec, text), which encodes some text and decodes it again. Your team has written a few codecs, and another library supplies more. You also want to give your own codecs a shared roundtrip helper. Should Codec be a protocol or an ABC? Here you want both, one for each job.

from abc import ABC, abstractmethod
from typing import Protocol


class Codec(Protocol):
    def encode(self, text: str) -> str: ...
    def decode(self, blob: str) -> str: ...


class BaseCodec(ABC):
    @abstractmethod
    def encode(self, text: str) -> str: ...

    @abstractmethod
    def decode(self, blob: str) -> str: ...

    def roundtrip(self, text: str) -> str:
        return self.decode(self.encode(text))


class ReverseCodec(BaseCodec):
    def encode(self, text: str) -> str:
        return text[::-1]

    def decode(self, blob: str) -> str:
        return blob[::-1]


def exchange(codec: Codec, text: str) -> str:
    return codec.decode(codec.encode(text))


print(exchange(ReverseCodec(), "hello"))  # hello
print(ReverseCodec().roundtrip("hello"))  # hello

A protocol says what the caller needs

Codec is a protocol. It lists the two methods that exchange calls. As the first article in this track showed, any object with matching encode and decode methods fits it. Its class does not have to inherit from Codec. So a codec from another library works with exchange as it is.

An ABC shares code and makes subclasses define the rest

BaseCodec is an ABC, an abstract base class from the abc module. ABCs use nominal typing, which matches by name. An object counts as a BaseCodec only if its class inherits from it.

An ABC does two jobs. Every subclass inherits the ABC's ordinary methods, such as roundtrip. And @abstractmethod marks the methods that each subclass must define for itself. Python refuses to create an instance of a subclass while any abstract method is still missing:

class HalfCodec(BaseCodec):
    def encode(self, text: str) -> str:
        return text


HalfCodec()
# TypeError: Can't instantiate abstract class HalfCodec without an
# implementation for abstract method 'decode'

Python makes that check when you create the object, and it looks only at method names. mypy does more. It reports HalfCodec() before you run anything, and it checks that each method's signature matches the base class.

Use both, one for each job

ReverseCodec inherits from BaseCodec, so it gets roundtrip without writing it. It also fits Codec, because it has both methods. exchange asks for Codec, so it accepts your codecs and a library's codecs alike.

roundtrip is not part of Codec, so exchange cannot call it. That is on purpose. List in the protocol only the methods its callers use, and put helpers on the base class.

Two common mistakes

Asking for the ABC when you only need its methods. Suppose exchange took codec: BaseCodec. Then mypy would reject a library's VendorCodec, even though it has matching encode and decode methods:

Argument 1 to "exchange" has incompatible type "VendorCodec"; expected "BaseCodec"

Accept the protocol instead.

Expecting a protocol to supply code. A class that only matches Codec gets no methods from it. To share code, use inheritance. A later exercise in this track shows that a class can also inherit from a protocol, and get the protocol's method bodies that way.

Check your understanding send_all(sink, messages) only calls sink.write(text), so should sink be your team's BaseSink ABC or a protocol? Show answer

A protocol with one write method. send_all only calls write, so asking for BaseSink would reject a test double or a library class that has a matching write. Your own sinks can still inherit from BaseSink to get its logging helpers, and they fit the protocol as well.

Choose by what the caller needs

You want to Use
Accept any object that has the right methods A protocol
Share code, and make subclasses define certain methods An ABC
Both A protocol for callers, and an ABC that your own classes inherit

Going further

Before you write a protocol, check whether the standard library already has one. Python 3.14 added io.Reader[T] and io.Writer[T] for simple streams:

from io import BytesIO, Reader


def read_prefix(source: Reader[bytes]) -> bytes:
    return source.read(4)


print(read_prefix(BytesIO(b"abcdef")))  # b'abcd'

SomeABC.register(SomeClass) makes isinstance(obj, SomeABC) return True for a class that does not inherit from the ABC. This is virtual registration. The class gets none of the ABC's methods, Python does not check that it defines the abstract ones, and mypy ignores the registration.

Some ABCs in collections.abc answer isinstance by looking for a method, through a hook called __subclasshook__. Iterable looks for __iter__. That check can disagree with iter(). A class that defines only __getitem__ works with iter(), yet isinstance(obj, Iterable) is False for it.

See the typing specification for protocols and the abc module documentation.

step 3 of 26

Interfaces with Protocols and ABCs

Up next Build an ABC with an Abstract Property and Classmethod