Python Type Hints - Duck typing with Protocol
Duck typing says “if it quacks like a duck, treat it like a duck.” Type checking seems at odds with duck typing: we restrict variables to named types (classes), allowing only that type or subtypes. This seems like it would stop us from passing in arbitrary objects that can “quack the right way”. But since PEP 0544, we can declare “duckish” types with
Take this example:
class Duck: def quack(self) -> str: return "Quack." def sonorize(duck: Duck) -> None: print(duck.quack()) sonorize(Duck())
Here we have a vanilla
Duck class and a function that accepts that class.
sonorize() is quite restrictive, and could better serve users by being duck-typed. It could accept anything with a correctly typed
quack() method and function just fine.
The way to specify this is with a
typing.Protocol type. A protocol type contains a set of typed methods and variables. If an object has those methods and variables, it will match the protocol type.
typing.Protocol was added in Python 3.8. On prior versions of Python, you can use
typing_extensions.Protocol, and migrate when you upgrade. Mypy has supported
Protocol since version 0.530 (October 2017).
In our example, we can define a protocol for objects that can quack:
from typing import Protocol class Quacker(Protocol): def quack(self) -> str: ...
class syntax to define a protocol, and add relevant members in the body with type hints. The methods will never be executed, so it’s standard to use Python’s ellipsis symbol for their bodies.
With the protocol defined, we can use it in
sonorize() to match any object with a
quack() method that returns a string. To combine it into a full example:
from typing import Protocol class Quacker(Protocol): def quack(self) -> str: ... class Duck: def quack(self) -> str: return "Quack." class MegaDuck: def quack(self) -> str: return "QUACK!" def sonorize(duck: Quacker) -> None: print(duck.quack()) sonorize(Duck()) sonorize(MegaDuck())
The Python documentation calls use of
Protocol structural subtyping, meaning the type checker compares types based upon their internal structure. This is opposed to nominal subtyping, meaning that the type checker compares types based on whether they are the named type, or a subclass.
The abstract base classes in Python’s
collections.abc module are useful for common protocol checks. For example, you can use
Sized to accept any type that works with
Iterable[int] to accept any iterable of integers.
If your Django project’s long test runs bore you, I wrote a book that can help.
One summary email a week, no spam, I pinky promise.
- Python Type Hints - How to Specify a Class Rather Than an Instance Thereof
- Python Type Hints - How to Use TypedDict
- Python Type Hints - How to Fix Circular Imports
Tags: mypy, python