Skip to content
← Back to explainers

Python explainer

Accept Any Object That Has the Right Methods

About 4 min to read. Allow extra time to work through examples.

Describe what a function needs from its arguments with a Protocol, so any class with the right methods fits, and read what mypy reports when one does not.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 1 of 26

Interfaces with Protocols and ABCs

Up next Write a Protocol for Three Unrelated Classes