Your cleanup code closes every resource that callers pass in, such as a log
file or a database connection. The classes share nothing except a close()
method. They come from different libraries, so you cannot give them a
common base class.
class LogFile:
def close(self) -> None:
print("log file closed")
class Connection:
def close(self) -> None:
print("connection closed")
class Timer:
def stop(self) -> None:
print("timer stopped")
def close_all(resources: list[object]) -> None:
for resource in resources:
resource.close()
mypy rejects the call resource.close(). For an object, it knows only
the methods that every Python object has, such as __repr__, so it
reports:
error: "object" has no attribute "close" [attr-defined]
What you want to write is "any object with a close() method that takes no
arguments and returns None".
Describe the methods with a Protocol
A protocol is a class that lists the methods an object must have. Any
class with matching methods fits it. To write one, inherit from
typing.Protocol and give each method ... as its body:
from typing import Protocol
class Closeable(Protocol):
def close(self) -> None: ...
def close_all(resources: list[Closeable]) -> None:
for resource in resources:
resource.close()
close_all([LogFile(), Connection()])
close_all([LogFile(), Timer()])
LogFile and Connection never mention Closeable. mypy accepts them
because each has a close method with the same signature. It rejects the
second call, because Timer has stop instead:
error: List item 1 has incompatible type "Timer"; expected "Closeable" [list-item]
Without the check, the second call would crash at runtime with
AttributeError: 'Timer' object has no attribute 'close'. mypy reports the
bug before the code runs.
Structural and nominal typing
This way of matching by shape is structural typing. mypy compares the members an object has with the members the protocol lists. The class can have other methods as well.
Ordinary classes use nominal typing, which means matching by name. A
function that takes an Animal accepts a Dog only if Dog inherits from
Animal, as in class Dog(Animal).
Structural typing suits classes from other libraries. You describe what your
function needs, and the classes that callers pass in stay as they are. The
standard library's type hints use protocols too. typing.SupportsInt is a
protocol for anything with an __int__ method, and mypy treats
collections.abc.Iterable as a protocol for anything with __iter__.
Common mistakes
A method with a different signature. A Socket whose method is
close(self, force: bool) does not fit Closeable, because close_all
calls close() with no arguments. mypy names the member that conflicts:
error: List item 0 has incompatible type "Socket"; expected "Closeable" [list-item]
note: Following member(s) of "Socket" have conflicts:
note: Expected:
note: def close(self) -> None
note: Got:
note: def close(self, force: bool) -> None
Give the parameter a default, force: bool = False, and Socket fits.
Testing a protocol with isinstance. A plain protocol exists for the
type checker only. isinstance(thing, Closeable) raises
TypeError: Instance and class checks can only be used with @runtime_checkable protocols,
and mypy reports the same mistake before the code runs. A later item in
this track covers @runtime_checkable and its limits.
Check your understanding
A Printer class has close(self) -> None and print_page(self, text: str) -> None. It does not inherit from anything. Does it fit Closeable?
Show answer
Hide answer
Yes. Closeable lists one method, and Printer has a close method with
the same signature. mypy checks only the members the protocol lists, so it
ignores the extra print_page method. Printer needs no base class,
because protocols match by shape.
Going further
A class can also inherit from a protocol, as in class LogFile(Closeable).
mypy then checks the class where you define it, and the class can inherit
method bodies from the protocol. A later exercise in this track does that.
Protocols can also list attributes, such as name: str, and take type
parameters. Both come up later in the track.
Protocols arrived in Python 3.8 with PEP 544. The typing specification's chapter on protocols has the full rules.