Protocols in Python

Author:Murphy  |  View: 23119  |  Time: 2025-03-23 13:15:32

Python 3.8 introduced a neat new feature: protocols. Protocols are an alternative to abstract base classes (ABC), and allow structural subtyping – checking whether two classes are compatible based on available attributes and functions alone. In this post we'll go into the details about this and show how to use protocols using practical examples.

Photo by Chris Liverani on Unsplash

Typing in Python

Let us begin by discussing how Python is typed. It is a dynamically typed language, meaning types are inferred at runtime and the following code runs without problems:

def add(x, y):
    return x + y

print(add(2, 3))
print(add("str1", "str2"))

The first call results in an integer addition returning 5, the second in a string concatenation returning "str1str2". This is different to e.g. C++, which is statically typed – and we have to provide type declarations:

int add(int x, int y) {
    return x + y;
}

std::string add(std::string x, std::string y) {
    return x + y;
}

int main()
{
    std::cout<

Static typing offers the advantage of having the potential to catching errors at compile time – whereas in dynamically typed languages we only encounter these during runtime. On the other hand, dynamic typing can allow quicker prototyping and experimentation – one reason why Python has become so popular.

Dynamic typing is also called duck typing, based on the saying: "if it walks like a duck and it quacks like a duck, then it must be a duck". Ergo: if objects offer the same attributes / functions, they should be treated similarly, and e.g. can be passed to functions requiring the other type.

Nevertheless, especially in larger, more professional software products, this unreliability offers more down- than upsides – and the trend thus goes towards static type checking, e.g. via providing type hints with mypy.

Subtyping

One interesting issue – hinted above e.g. in the short paragraph about duck typing – is subtyping. If we have a function with signature foo(x: X), what other classes except X does mypy allow to be passed to the function? (Note we now only question typing and type hints – since Python is dynamically typed, as discussed before, we can pass any object to foo – and if the expected attributes / methods are there, it works, and crashes otherwise.) For this, one distinguishes structural and nominal subtyping. Structural subtyping is based on class hierarchy / inheritance: if class B inherits from A, it is a subtype of A – and thus can also be used everywhere where A is expected. On the other hand, nominal subtyping is defined based on operations available for the given class – if B offers all attributes / functions provided by A, it can be used in all places where A is expected. One can argue, that the latter method is more "Pythonic", as it more resembles the idea of duck typing.

Subtyping in Practise

Before Python 3.8, one could only use inheritance for subtyping, and e.g. use (abstract) base classes (ABCs): here, we define an (abstract) base class – a "blue print" for child classes – and then define several child classes inheriting from this:

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def feed(self) -> None:
        pass

class Duck(Animal):
    def feed(self) -> None:
        print("Duck eats")

def feed(animal: Animal) -> None:
    animal.feed()

duck = Duck()
feed(duck)

In this code, we first define the ABC Animal, providing the abstract method feed. Then, we subclass Duck and implement this method. Lastly we define a generic feed function, accepting an Animal as parameter, and then feed this animal.

This seems legit – where is the problem? Indeed there are a few reasons, why one might not want to use this pattern: base classes are often poorly exposed and hard to include – i.e. if you want to inherit from a base class in another module, maybe a public library, you would have to find the base class. Second, you cannot change existing code e.g. in public / third party modules: this is a problem if you would like types imported from those to be subtypes of others, possibly in combination with other types introduced by you. And lastly, this somewhat goes against the idea of Python and duck typing.

Protocols

Thus, Python 3.8 introduced protocols, alleviating aforementioned issues. Protocols, as the name suggests, work implicitly by defining an "interface" of expected attributes / methods, and checking whether the classes in question provide these, if necessary:

from typing import Protocol

class Animal(Protocol):
    def feed(self) -> None:
        pass

class Duck:
    def feed(self) -> None:
        print("Duck eats")

def feed(animal: Animal) -> None:
    animal.feed()

duck = Duck()
feed(duck)

As we can see, Animal now is a Protocol, and Duck does not inherit from any base class – still mypy is happy.

Subclassing Protocols

Naturally, we can also subclass protocols – i.e. defining a child protocol inheriting from a parent one, extending this. When doing so, we just need to remember to inherit both from the parent protocol, as well as typing.Protocol:

from typing import Protocol

class Animal(Protocol):
    def feed(self) -> None:
        pass

class Bird(Animal, Protocol):
    def fly(self) -> None:
        pass

class Duck:
    def feed(self) -> None:
        print("Duck eats")

    def fly(self) -> None:
        print("Duck flies")

def feed(animal: Animal) -> None:
    animal.feed()

def feed_bird(bird: Bird) -> None:
    bird.feed()
    bird.fly()

duck = Duck()
feed_bird(duck)

In the above code, we specify Bird as a specific Animal type, and then define a function to feed a bird, expecting it to fly away afterwards.

A Short History of Protocols

All code written above is valid Python, even before Python 3.8 (remember, Python is dynamically typed) – we only needed ABCs to satisfy mypy, and it would have complained for the latter examples without ABCs. Nevertheless, protocols have been around in Python for much longer, just not as visible or explicit as now: most Python developers used the term "protocol" as an agreement to conform to certain interfaces, just as was made explicit now. One famous example is the iterator protocol – an interface describing which methods a custom iterator needs to implement. To make this work with mypy without explicit protocols, several "tricks" existed, such as custom typing types:

from typing import Iterable

class SquareIterator:
    def __init__(self, n: int) -> None:
        self.i = 0
        self.n = n

    def __iter__(self) -> "SquareIterator":
        return self

    def __next__(self) -> int:
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i**2
        else:
            raise StopIteration()

def iterate(items: Iterable[int]) -> None:
    for x in items:
        print(x)

iterator = SquareIterator(5)
iterate(iterator)

Abstract Base Classes vs Protocols

We already discussed possible drawbacks of ABCs (difficulty with external modules and interfaces, "unpythonic" code). However, protocols should not replace ABCs, but instead both be used. ABCs e.g. are a good way of re-using code: all common functionality should be implemented in the base class, and only specific bits in the child classes. This is not possible with protocols.

Conclusion

In this post we discussed static vs dynamic typing (duck typing), and how mypy handles subtyping. Before Python 3.8, abstract base classes (ABCs) had to be used. With the introduction of protocols, Python obtained an elegant way of defining "interfaces" and allowing mypy to check adherence to these: in a more Pythonic style, protocols allow specifying which attributes / functions classes need to implement, and then allowing the usage of all such classes as subtypes of the protocol.

Tags: Programming Python Python Programming Python3 Software Development

Comment