Protocol typing was introduced in Python 3.8 in PEP 544 and is supported by the two big players in Python static analysis -- MyPy and Pyright. It enables developers to type by what objects do rather than what objects inherit. For a brief example so this hopefully starts to make sense, lets make one.
from typing import Protocol
class Readable(Protocol):
def read(self) -> str: ...
Our protocol is relatively simple, a Readable
object has to have a read()
method that returns a str
. Straight away we can
use this interface in a function, even though we haven't written any implementations yet.
def write_to_file(reader: Readable, filepath: str) -> None:
with open(filepath, "w") as f:
# as reader is Readable, it must have .read() -> str
f.write(reader.read())
If we run MyPy on the code we get
no issues and a gold star. As we expect a Readable
object to have a .read()
function, this is fully typesafe. Now lets try to use
this function.
For this lets start by using a object that Python already has builtin. Lets open the source file, and write it to another file.
# Note: __name__ is the absolute path to the source file
with open(__name__, "r") as file:
write_to_file(file, "file.out")
Again, MyPy gives us the all clear as
file
object has a .read()
method that returns a str
. So you can start to see that Readable
is a Type that ensures an object
has the structure you have defined. Lets make our own class follow the Readable
protocol to understand why this is powerful in
contrast to inheritance.
We are implementing an object to represent a website. This takes in a url, and on read will make a http request using the requests library, returning the page content as text.
import requests
class Website: # Notice no inheritance
def __init__(self, url: str) -> None:
self.url = url
def read(self) -> str:
response = requests.get(self.url)
response.raise_for_status()
return response.text
As our Website
object implements the Readable
protocol by having the def read(self) -> str:
method signature, we can use it in
our write_to_file
function.
my_website = Website("https://h.pythondiscord.com")
write_to_file(my_website, "website.out")
Once again, MyPy is happy with this. As you can see, we are able to use two completely different implementations that have no inherited functionality in the same function while remaining typesafe.
Resources
- https://docs.python.org/3/library/typing.html#typing.Protocol
- https://mypy.readthedocs.io/en/stable/protocols.html
- https://github.com/microsoft/pyright/blob/main/docs/features.md#type-checking-features
- https://peps.python.org/pep-0544/