🎓 A1Learner
🔍

Python Protocols

A Protocol is a class that names the methods a shape must offer.

Name the shape

Drawable asks for one method, draw. Circle offers it without inheriting anything.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "circle"

def render(item: Drawable) -> str:
    return item.draw()

print(render(Circle()))

No inheritance needed

Square is unrelated to Circle. Both fit because both offer draw. The last line shows Circle has only object as its parent.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "circle"

class Square:
    def draw(self) -> str:
        return "square"

def render(item: Drawable) -> str:
    return item.draw()

print(render(Circle()))
print(render(Square()))
print(Circle.__bases__)

A plain Protocol blocks isinstance

isinstance needs a runtime check first. The except block catches the TypeError and prints its name.

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "circle"

try:
    print(isinstance(Circle(), Drawable))
except TypeError as error:
    print(type(error).__name__)

Add runtime_checkable

The decorator switches on isinstance checks. Python only checks that draw exists here.

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def draw(self) -> str:
        return "circle"

print(isinstance(Circle(), Drawable))
print(isinstance(42, Drawable))

Example

from typing import Protocol, runtime_checkable


@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> str:
        ...


class Circle:
    def draw(self) -> str:
        return "Drawing a circle"


class Square:
    def draw(self) -> str:
        return "Drawing a square"


def render_all(items: list[Drawable]) -> None:
    for item in items:
        print(item.draw())


shapes: list[Drawable] = [Circle(), Square()]
render_all(shapes)
print(isinstance(Circle(), Drawable))
print(isinstance(42, Drawable))

Key points

  • Structural typing: matching a value by its methods, not by its parent class
  • Protocol: a class that names the methods a shape must offer
  • Use a Protocol when unrelated classes should fit one hint
  • Use runtime_checkable to allow isinstance checks with a Protocol
  • Use an ABC when subclasses must inherit and share code
  • Use a Protocol as a hint, not as an object to create
  • With runtime_checkable, Python only checks that the methods exist

In this example, Drawable is a runtime checkable Protocol. It asks only for a draw method. Circle and Square offer draw but share no parent class. render_all accepts a list of Drawable values. It prints the draw result of each shape. isinstance accepts Circle because it has draw. It rejects 42 because 42 has no draw method.

Note

It does not check their hints or bodies.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Python Playground

Exercise

A class does not inherit from Drawable. When can it still fit a Drawable hint?