Table of Contents

Python protocols

Python protocols (from typing.Protocol) define structural types—what methods and attributes an object must have, without requiring explicit inheritance. A class conforms to a protocol if it implements the right methods, whether or not it inherits from the protocol class. Protocols enable duck typing with static type checking.

Use protocols to define interfaces based on what objects can do, not what they inherit from.

Example

This example shows protocols enabling structural typing without inheritance.

# run: python3 protocols.py
# description: structural typing with protocol
 
from typing import Protocol, runtime_checkable
 
# Define protocol (structural interface)
@runtime_checkable
class Drawable(Protocol):
    """Anything with a draw method."""
    def draw(self) -> None:
        ...
 
class Circle:
    def draw(self):
        print("Drawing circle")
 
class Square:
    def draw(self):
        print("Drawing square")
 
class Triangle:
    """Doesn't inherit from Drawable, but implements draw()."""
    def draw(self):
        print("Drawing triangle")
 
# All conform to protocol, no inheritance needed
def render(obj: Drawable):
    obj.draw()
 
render(Circle())
render(Square())
render(Triangle())
 
# Runtime checking
circle = Circle()
print(f"Circle is Drawable: {isinstance(circle, Drawable)}")
 
# Protocol with multiple methods
@runtime_checkable
class Serializable(Protocol):
    def to_dict(self) -> dict:
        ...
 
    def from_dict(self, data: dict) -> None:
        ...
 
class User:
    def __init__(self, name):
        self.name = name
 
    def to_dict(self):
        return {"name": self.name}
 
    def from_dict(self, data):
        self.name = data["name"]
 
def save(obj: Serializable):
    return obj.to_dict()
 
user = User("Alice")
print(f"Saved: {save(user)}")
print(f"User is Serializable: {isinstance(user, Serializable)}")
 
# Generic protocol
from typing import TypeVar, Generic
 
T = TypeVar('T')
 
class Container(Protocol[T]):
    def add(self, item: T) -> None:
        ...
 
    def get(self) -> T | None:
        ...
 
class Stack:
    def __init__(self):
        self.items = []
 
    def add(self, item):
        self.items.append(item)
 
    def get(self):
        return self.items.pop() if self.items else None
 
s: Container[int] = Stack()
s.add(42)
print(f"Stack value: {s.get()}")

Common patterns

Defining protocols:

Runtime checkability:

Methods in protocols:

Generic protocols:

Combining protocols:

Implicit vs explicit conformance:

Static vs runtime checking:

Partial protocols:

When to use:

When ABCs are better: