GDWR

Protocol Typing

Published on
Updated on
Edit on Github

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/