Protocols: structural typing

typing · Practical · 12 min

Protocols let functions accept any compatible object without forcing a shared base class.

Classic typing is nominal: a Dog is an Animal only if it subclasses Animal. Python code is usually duck-typed: it only cares that an object has the right methods. typing.Protocol brings duck typing to the type checker.

class SupportsClose(Protocol):
    def close(self) -> None: ...

Any class with a compatible close method matches SupportsClose, with no inheritance or registration. This is structural typing. The standard library uses it heavily: Iterable, Sized, Hashable and SupportsInt are protocols.

Tips:

  • Keep protocols small, describing only what your function needs.
  • Attributes can be protocol members too: name: str.
  • Protocols can be generic: class Reader[T](Protocol): def read(self) -> T: ....
  • A callback protocol with __call__ types callables that need keyword arguments or overloads, which Callable can't express.

By default protocols are only for the checker. Decorate one with @runtime_checkable to allow isinstance(obj, Proto), which only checks that the members exist, not their signatures, and can be slow.

Use a Protocol for what a function needs from its argument; use an ABC when you want to share implementation or force explicit opt-in.

Example

from typing import Protocol, runtime_checkable

@runtime_checkable
class Shape(Protocol):
    name: str
    def area(self) -> float: ...

class Square:                       # no inheritance from Shape
    name = "square"
    def __init__(self, side: float) -> None:
        self.side = side
    def area(self) -> float:
        return self.side ** 2

class Label:
    name = "label"

def report(shapes: list[Shape]) -> list[str]:
    return [f"{s.name}: {s.area():.1f}" for s in shapes]

print(report([Square(2)]))                         # ['square: 4.0']
print(isinstance(Square(1), Shape), isinstance(Label(), Shape))  # True False
# report([Label()])  -> checker error: "Label" is missing "area"

Remember

  • A Protocol matches any class with compatible members, no inheritance needed
  • Keep protocols small and focused on what the function uses
  • @runtime_checkable enables isinstance, checking member presence only

Watch out for

  • Relying on runtime_checkable isinstance to verify method signatures; it only checks names exist.

Check yourself

  1. Class Duck has quack(self) -> None but doesn't inherit anything. Does it match class Quacker(Protocol): def quack(self) -> None: ...?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/typing.html#typing.Protocol