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.
I think the easiest way to understand protocols is to write some code, so let's dive in. First, we define a protocol
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
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.
NonPrintable is underlined and if hovered we can see this error message:
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.
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.
Well, that doesn't look very good.
Okay, so a protocol doesn't work in the
isinstance check. Let's look at the other error message as well.
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
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
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.