A First Look at Python Protocols (PEP 544)
Tags: python, typechecking, programming, coding
Published: 2020-10-05 05:48:42

I really like the type hints added to python 3.5, [PEP 484](https://peps.python.org/pep-0484/), but somehow I totally missed [PEP 544](https://peps.python.org/pep-0544/) - _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`:
```python
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.
```python
class MyPrintable:
    def print(self) -> None:
        print("Hello DEV!")

class NonPrintable:
    pass
```
I am using Visual Studio Code with the [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-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.
```python
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)](https://dev-to-uploads.s3.amazonaws.com/i/nk5brs912zn4i8qfq9kz.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/9asrmbm1inyrr04qkoa2.png)

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:

```python
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](https://dev-to-uploads.s3.amazonaws.com/i/vyto4grhez1ihjb4ekfh.png)

Well, that doesn't look very good.

![Hovering Printable in the isinstance call, showing error message](https://dev-to-uploads.s3.amazonaws.com/i/zitn8drs31p1rrs58kdu.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/snasn1g0ju7kwuhcdhq7.png)

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:
```python
@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](https://peps.python.org/pep-0544/) 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.