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 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.
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
).
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.
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 favor 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 favor of abc
.
Protocols allow you to define an interface for a function (not only a class).
It is a very cool feature that is worthy of a separate post.
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.
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.
Let’s add up the scores:
Capability |
ABC |
Protocols |
---|---|---|
Runtime checking |
1 |
1 |
Static checking |
1 |
1 |
Explicit interface with inheritance |
1 |
1 |
Implicit interface without inheritance ( |
0.5 |
1 |
Ability to have default method implementation |
-1 |
-1 |
Callback interface |
0 |
1 |
Number of lines |
-1 |
0 |
|
|
|
Total |
1.5 |
4 |
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 here.