Python interfaces: abandon ABC and switch to Protocols

Python interfaces: abandon ABC and switch to Protocols

Several practical reasons to prefer new shiny Protocols

I used a standard Python library abc to define interfaces for the last 10 years of my career. But recently, I found that relatively new Python Protocols are way nicer. People find uses for both technologies. But I want to convince you to completely jump ships and start using them instead of more traditional techniques.

Python interfaces: what can you use?

Python is somewhat different from other popular languages since there are no interfaces on a language level. However, there are several library implementations:

  • abc

  • typing.Protocols

  • third-party implementations like Zope

  • custom implementations (e.g. via metaclasses)

abc package is probably the most popular:

from abc import ABC, abstractmethod

class Animal(ABC):
   @abstractmethod
   def eat(self, food) -> float:
       pass

   @abstractmethod
   def sleep(self, hours) -> float:
       pass

Next, the most frequently mentioned package seems to be Zope:

from zope.interface import Interface

class Animal(Interface):
   def eat(self, food) -> float:
       pass

   def sleep(self, hours) -> float:
       pass

Zope is a web-related library, and its interfaces have a lot of advanced features.

Also, there are more custom packages and tutorials on the web on how to make an interface system yourself (see example).

Finally, there are protocols:

from typing import Protocol

class Animal(Protocol):
   def eat(self, food) -> float:
       ...

   def sleep(self, hours) -> float:
       ...

A protocol is a formalization of Python's “duck-typing” ideology. There are many great articles on structural typing in Python (this, that and some discussion). Maybe protocols and interfaces are theoretically different beasts, but a protocol does the job. I had great success replacing abc with Protocols without any downsides.

What should you use?

You should know that an interface system will not be localized to a small part of your codebase. After you choose to go with one, you’ll see it everywhere, and it's going to be hard to change in the future. So I would immediately dismiss any custom implementations or Zope. It’s an extra dependency you have to deal with forever: installation, versions, support, and so on. For example, you have to install a Mypy plugin to support a zope.interface well. Additionally, a new developer in the team might not know this custom package, and you'll have to explain what it is and why you chose it.

The main battle will happen between abc and Protocols. But if you really want a zope vs Protocols battle, please read this (it has a detailed analysis of the runtime benefits of zope).

Static checking first

The big assumption I’m going to make is that you’re already convinced that static checking is a must: you are not going to run the code that fails pylint/mypy. Both checkers support abc and Protocols equally well. Also, just know that both abc and Protocols allow runtime checking, in case you need it.

Both support explicit syntax

First, note that you still can explicitly inherit from an abc and a Protocol. Many arguments in a very good video (and comments) from Arjan revolve around the misconception that you can’t do that with protocols. You totally can:

class Giraffe(Animal):
   ...

So in that regard, abc and protocols could be used the same way. However, Protocols give you an extra degree of design freedom by default. You can avoid explicit inheritance but still enjoy full interface checking:

class Giraffe:  # no base class needed!
   def eat(self, food) -> float:
       return 0.

   def sleep(self, hours) -> float:
       return 1.

def feed_animal(animal: Animal):
    ... 

giraffe = Giraffe()
feed_animal(giraffe)

This allows you to make an interface for the code you don’t control and loosen the dependencies between modules in your codebase. Whether to choose an implicit or explicit option is a subtle choice decided on a case-by-case basis. A good example in favour of explicit "opt-in" for an interface is described here. Protocols do not force you to opt-in, but you can establish a company-wide rule to explicitly inherit from any protocol.

abc also support implicit interfaces through the concept of "virtual subclasses". But you have to call register for every implementation:

class Giraffe:  # no base class needed!
   def eat(self, food) -> float:
       return 0.

class Animal(ABC):
    ...

Animal.register(Giraffe)  # achieves the same as implicit Protocol

Procotol supports implicit and explicit variants without extra syntax and works with mypy. Also, mypy does not support register as of the end of 2022. I'm not sure if we can fully count that in favour of abc.

Protocols allow you to define an interface for a function (not only a class) - see callback protocols. It is a very cool feature that is worthy of a separate post.

Both support default methods :(

Unfortunately, there is a big downside to both abc and Protocols. In the real world, many people work in a single codebase. Abstract base classes sometimes tend to acquire default method implementations. This is what it might look like:

class Animal(Protocol):  # the same holds for Animal(ABC):
   def eat(self, food) -> float:
       ...  # this is still abstract

   def sleep(self, hours) -> float:
       return 3.

In that case, they stop being “abstract” and become just base classes. Python and static checkers do not catch that. A software design with inheritance is not really the same as a design with interfaces. I would love Python to separate them on a language level, but it is unlikely to happen. Implicit protocols have an advantage here. They allow you to avoid messy inheritance altogether.

Protocols are shorter

Last but not least, you can count the number of lines of code you need to define an interface. With abc, you must have an abstractmethod decorator for every method. But with Protocols without runtime checking, you don't have to use any decorators at all. So here, Protocols win hands down.

Conclusion

Let’s add up the scores:

CapabilityABCProtocols
Runtime checking11
Static checking11
Explicit interface with inheritance11
Implicit interface without inheritance (abc requires register)0.51
Ability to have default method implementation-1-1
Callback interface01
Number of lines-10
Total1.54

Hopefully, I’m not missing anything huge in this analysis. Thank you for reading! Looking at the results, team "Protocols" wins, and you probably should just start using it!

Thank you for reading! You can find me on LinkedIn or Twitter.

Originally published at sinavski.com.