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?