ABC vs Protocol in Python
Before typing
was released for Python, the ABC
class reigned as champion for describing shape and behaviors of classes. After type annotations, ABC
and @abstractmethod
were still used to describe the behaviors: they felt ‘interface-like’.
Then, Protocol
was released and introduced a new way for declaring class behaviors.
Should you use ABC or Protocol?
Before describing why or why not you should choose one over the other, let’s delve into what each of them are.
What is ABC or Abstract Base Classes?
Abstract Base Classes, or ABC, is a a module that provides infrastructure to define abstract classes in Python.
As previously stated, they predated type hinting and were the go-to for enforcing class behaviors. Their goal was to provide a standardized means to test if an object adhered to a given specification.
For example,
1from abc import ABC, abstractmethod
2
3
4class Readable(ABC):
5 @abstractmethod
6 def read(self):
7 pass
8
9
10class Stream(Readable):
11 def read(self):
12 return "read!"
13
14
15class FileReader(Readable):
16 def read(self):
17 return "some lines from a file"
ABCs use nominative subtyping by default: ie. you must explicitly inherit from a superclass to be considered a subtype.
Although, they do have a register
method that enables a structural-like typing:
1from abc import ABC, abstractmethod
2
3
4class Readable(ABC):
5 @abstractmethod
6 def read(self):
7 pass
8
9
10class Stream:
11 def read(self):
12 return "read!"
13
14
15Readable.register(Stream)
What is Protocol
Protocols are intended to make static checking easier - without needing to use an isinstance
check at runtime.
They are treated as formalized duck typing - ie. the class only has to have the same attributes and methods… NOT be an instanceof
.
Taking our class from earlier,
1from typing import Protocol
2
3
4class Readable(Protocol):
5 def read(self) -> str: ...
We can implement the Protocol
in different ways:
1class Stream:
2 def read(self) -> str:
3 return "read!"
OR explicitly
1class Stream(Readable):
2 def read(self) -> str:
3 return "read!"
Additionally, we can enforce runtime evaluation using the runtime_checkable
decorator:
1from typing import Protocol, runtime_checkable
2
3
4@runtime_checkable
5class Readable(Protocol):
6 def read(self) -> str: ...
7
8
9class Stream(Readable):
10 def read(self) -> str:
11 return "read!"
Which is preferred?
From my perspective, Protocol
wins. Given Python’s emphasis on duck typing (“if it walks and quacks like a duck, then it must be a duck”), it makes sense to opt for Protocol
.
If there is a need to enforce runtime checks, it has that covered with the @runtime_checkable
.
Protocol Pros:
- Short syntax
- Aligns with the “duck typing” mentality
- Supports implicit or explicit declaration
- Supports runtime enforcement
Protocol Cons:
- Only available in Python 3.8 or above