A First Look at Python Protocols (PEP 544)

I really like the type hints added to python 3.5, PEP 484, but somehow I totally missed PEP 544 - Protocols: Structural subtyping (static duck typing). Protocols were added to python 3.8 which, while it is the current stable version of Python, is almost a year old. We are very much closing in on Python 3.9, the release is planned for the fifth of October. In this post, I will take a first look at protocols and how they work with linting using Pylance in VS Code.

The title of the PEP says static duck typing, which is actually a really good description of protocols. They allow you to define an interface/template which an object has to fulfill, and if does it is said to be an instance of the protocol. Protocols are just checked by type-checking tools and aren't enforced at run-time.

sidenote: For others like me who have been playing around with TypesScript (TS), protocols are really similar to the interfaces of TS.

Type Checking Protocols in VS Code

I think the easiest way to understand protocols is to write some code, so let's dive in. First, we define a protocol Printable:

from abc import abstractmethod
from typing import Protocol
class Printable(Protocol):
    @abstractmethod
    def print(self) -> None:
        raise NotImplementedError

As you can see this is a class which inherits from the class Protocol. It has a single abstract method print, which takes no arguments and returns None. Note that in python None is returned if a function doesn't have a return statement.

Let's define a class that fulfills this protocol and a class that doesn't.

class MyPrintable:
    def print(self) -> None:
        print("Hello DEV!")

class NonPrintable:
    pass

I am using Visual Studio Code with the Pylance plug-in. With Pylance you can use the setting python.analysis.typeCheckingMode. This can be either off, basic or strict; I am currently running using basic. The screenshots in this post use these settings.

Now let's define a function that takes a Printable and see how the editor reacts to the two classes just created.

def simple_print(printable: Printable):
    printable.print()

If we pass an instance of MyPrintable and then NonPrintable to the function this is what shows up in the editor.

simple_print with MyPrintable (not underlined) and with NonPrinatable (underlined)

NonPrintable is underlined and if hovered we can see this error message:

Showing the type error NonPrintable is not assignable to parameter of type Printable

Both Mypy and Pylance complains about how NonPrintable doesn't fulfill the Printable protocol. This also means that the MyPrintable object is correctly identified as fulfilling the protocol, no inheritance needed.

sidenote: I am really enjoying Pylance, look at that error message! It is just fantastic!

Like I mentioned earlier, at runtime this information isn't used. However, the program would, of course, crash when we try to call the print-method on NonPrintable as it doesn't exist.

Runtime Type Checking

In python, we commonly use the isinstance function to verify if an object is an instance of a class. Does this work for protocols as well? I guess we have to try it out and to do so, I wrote this:

def my_print(printable: Printable):
    if isinstance(printable, Printable):
        print("It is a printable!")
        printable.print()
    else:
        print("boooo, not a printable")

my_print(MyPrintable())
my_print(NonPrintable())

Now let's see what Pylance thinks about this.

Function with isinstance and Printable underlined

Well, that doesn't look very good.

Hovering Printable in the isinstance call, showing error message

Okay, so a protocol doesn't work in the isinstance check. Let's look at the other error message as well.

Hovering isinstance function, showing error message suggesting using @runtime_checkable decorator for the Printable protocol

Interesting, so to check an instance against the protocol using isinstance, we need to decorate our protocol with @runtime_checkable. Note, that you would be informed by this if you run the code as well. It would report this error at runtime:

TypeError: Instance and class checks can only be used with @runtime_checkable protocols

According to the error all we need to do is add the decorator to the class, like this:

@runtime_checkable 
class Printable(Protocol):
    @abstractmethod
    def print(self) -> None:
        raise NotImplementedError

Now running the same code will show this output:

It is a printable!
Hello DEV!
boooo, not a printable

Final Notes

PEP 544 gives us protocols that allow us to define what requirements a function or class has for a parameter. Even more interesting, it allows us to do this without using inheritance while still working with isinstance.

This was just a first look at protocols and there is a lot more to explore. For example, they support defining instance and class variables. Protocols can also be generic, extended, and merged. This adds a really interesting tool for writing type-checked Python and it will be fun to experiment with it in a future project.