Site Tools


python-protocols

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:

  • class MyProtocol(Protocol):: define structural type
  • Methods with ... body (or pass): no implementation needed
  • Static type checkers infer protocol from method signatures

Runtime checkability:

  • @runtime_checkable: enable isinstance() checks
  • Without it, protocols only work with static type checkers
  • Runtime checking is stricter; must match exactly

Methods in protocols:

  • Just method signatures; no implementation (though can provide default)
  • Methods are checked by presence and signature, not behavior
  • If protocol requires def foo(self, x: int) -> str:, any class with that method conforms

Generic protocols:

  • class Iterable(Protocol[T]):: protocol parameterized by type
  • Useful for containers, iterators, transformations

Combining protocols:

  • Multiple Protocol inheritance in class definition
  • Class must implement all methods from all protocols

Implicit vs explicit conformance:

  • Implicit: class has right methods → conforms (duck typing + type checking)
  • Explicit: class Foo(ProtocolName):: but usually not needed
  • Point of protocols: conformance by structure, not inheritance

Static vs runtime checking:

  • Static: mypy, pyright etc. check at development time
  • Runtime: @runtime_checkable + isinstance() for checks at execution
  • Most code uses static checking; runtime is for introspection

Partial protocols:

  • Can define @runtime_checkable protocol subset of larger interface
  • Class implements more than protocol requires; still conforms

When to use:

  • Defining callback function signatures (functions called by framework)
  • Plugin interfaces without inheritance overhead
  • Type-safe duck typing with static type checking
  • Generic data structures (Stack, Queue, Cache)

When ABCs are better:

  • Need default implementations
  • Want to prevent direct instantiation
  • Need enforcement at runtime beyond type hints
  • Class hierarchy makes logical sense
python-protocols.md · Last modified: by 127.0.0.1