In most type systems, a value can only be a Duck if its class explicitly inherits from Duck. Python’s type system can do that and the opposite — accept any value that “looks like” a Duck (has the right methods), regardless of where it came from.

That’s called structural typing, or in Python culture, duck typing. The mechanism is the Protocol class.

The problem Protocols solve

You want a function that accepts anything with a .name attribute and an .area() method:

def describe(shape) -> str:
    return f"{shape.name} has area {shape.area()}"

Without types, this works for any object with the right shape. With types, you might naively write:

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


def describe(shape: Shape) -> str:
    return f"{shape.name} has area {shape.area()}"

Now any class that wants to be passed to describe must explicitly inherit from Shape. That’s annoying — what if the class comes from a library you don’t control?

Protocols — fix it

from typing import Protocol


class HasArea(Protocol):
    name: str
    def area(self) -> float: ...


def describe(shape: HasArea) -> str:
    return f"{shape.name} has area {shape.area()}"

Now any class with a name attribute and an area method satisfies HasArea — no inheritance required.

class Circle:
    def __init__(self, radius: float) -> None:
        self.name = "circle"
        self.radius = radius

    def area(self) -> float:
        return 3.14159 * self.radius ** 2


class Square:
    def __init__(self, side: float) -> None:
        self.name = "square"
        self.side = side

    def area(self) -> float:
        return self.side ** 2


print(describe(Circle(5)))    # circle has area 78.53975
print(describe(Square(4)))    # square has area 16

Neither Circle nor Square inherits from HasArea. Pyright still accepts them — because they have the right shape.

Protocols throughout the standard library

You’ve already used Protocols without knowing — Iterable, Sized, Hashable (in collections.abc) are all protocols. When you wrote:

def total_length(words: Iterable[str]) -> int:
    return sum(len(w) for w in words)

You were asking for “anything iterable over strings” — not “specifically a list, tuple, or anything that inherits from Iterable”. Lists, tuples, sets, generators all satisfy it because they have an __iter__ method.

When to use a Protocol

  • You want to accept any object with this shape, not just descendants of a specific class
  • You’re writing a library that should work with user types it can’t anticipate
  • You want to express “duck typing” formally for the type checker

For internal code where you control the classes, inheritance from a regular base class is often simpler. Protocols shine at boundaries between codebases.

Runtime-checkable Protocols

Normally, isinstance(x, MyProtocol) raises a TypeError. To enable runtime checks, add the decorator:

from typing import Protocol, runtime_checkable


@runtime_checkable
class HasName(Protocol):
    name: str


if isinstance(some_value, HasName):
    print(some_value.name)

This makes runtime type checks possible — but it only checks method existence, not method signatures. Use sparingly.

A practical example — a logger

A common pattern: accept any object with a log method, without forcing inheritance:

from typing import Protocol


class Logger(Protocol):
    def log(self, message: str) -> None: ...


def process_items(items: list[int], log: Logger) -> None:
    log.log(f"Processing {len(items)} items")
    for item in items:
        log.log(f"  item: {item}")

Any class with a log(self, message: str) -> None method works:

class ConsoleLogger:
    def log(self, message: str) -> None:
        print(message)


class FileLogger:
    def __init__(self, path: str) -> None:
        self.path = path

    def log(self, message: str) -> None:
        with open(self.path, "a") as f:
            print(message, file=f)


process_items([1, 2, 3], ConsoleLogger())
process_items([1, 2, 3], FileLogger("out.log"))

Neither logger needs to inherit from anything. The Protocol describes what’s needed; any class that fits is accepted.

What’s next

Final lesson of the section — a brief look at abstract base classes and multiple inheritance, including when (rarely) to use each.

Toggle theme (T)