Skip to main content Brad's PyNotes

PEP 544: Protocols - Structural Subtyping (Static Duck Typing)

TL;DR

PEP 544 introduces Protocol classes that enable structural subtyping (static duck typing) - type checking based on what methods an object has rather than its inheritance hierarchy, making Python’s type system more flexible and duck-typing friendly.

Interesting!

Protocols let you define “interfaces” that classes can implement just by having the right methods - no explicit inheritance needed! A class becomes a valid protocol implementation simply by having the required methods with correct signatures.

What Are Protocols?

Structural vs Nominal Typing

python code snippet start

from typing import Protocol

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

# No inheritance needed!
class Circle:
    def draw(self) -> None:
        print("Drawing a circle")

class Square:
    def draw(self) -> None:
        print("Drawing a square")

# Both work with functions expecting Drawable
def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())  # ✓ Works - has draw() method
render(Square())  # ✓ Works - has draw() method

python code snippet end

Generic Protocols

Iterator Protocol Example

python code snippet start

from typing import Protocol, TypeVar, Iterator

T = TypeVar('T')

class Iterable(Protocol[T]):
    def __iter__(self) -> Iterator[T]:
        ...

class NumberRange:
    def __init__(self, start: int, end: int):
        self.start = start
        self.end = end
    
    def __iter__(self) -> Iterator[int]:
        current = self.start
        while current < self.end:
            yield current
            current += 1

# NumberRange automatically satisfies Iterable[int]
def process_numbers(items: Iterable[int]) -> int:
    return sum(items)

result = process_numbers(NumberRange(1, 5))  # ✓ Type-safe

python code snippet end

Runtime Checking

@runtime_checkable Decorator

python code snippet start

from typing import Protocol, runtime_checkable

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

class Circle:
    def draw(self) -> None:
        print("Circle")

class NotDrawable:
    pass

# Runtime checking
print(isinstance(Circle(), Drawable))      # True
print(isinstance(NotDrawable(), Drawable)) # False

# Type-safe checking
if isinstance(obj, Drawable):
    obj.draw()  # Type checker knows obj has draw()

python code snippet end

Real-World Protocol Examples

File-Like Protocol

python code snippet start

from typing import Protocol

class Readable(Protocol):
    def read(self, size: int = -1) -> str:
        ...

class Writable(Protocol):
    def write(self, data: str) -> int:
        ...

# Works with files, StringIO, custom classes
def copy_text(src: Readable, dst: Writable) -> None:
    data = src.read()
    dst.write(data)

# Usage
import io
copy_text(open('input.txt'), open('output.txt', 'w'))
copy_text(io.StringIO('hello'), io.StringIO())

python code snippet end

Context Manager Protocol

python code snippet start

class ContextManager(Protocol[T]):
    def __enter__(self) -> T:
        ...
    
    def __exit__(self, exc_type, exc_value, traceback) -> bool | None:
        ...

class DatabaseConnection:
    def __enter__(self) -> 'DatabaseConnection':
        print("Connecting to database")
        return self
    
    def __exit__(self, exc_type, exc_value, traceback) -> None:
        print("Closing database connection")

# Automatically works with 'with' statements
with DatabaseConnection() as db:
    pass  # Use database

python code snippet end

Benefits of Protocols

Flexibility Without Inheritance

python code snippet start

# Traditional approach - rigid inheritance
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self) -> str:
        pass

class Dog(Animal):  # Must inherit
    def make_sound(self) -> str:
        return "Woof!"

# Protocol approach - structural typing
class SoundMaker(Protocol):
    def make_sound(self) -> str:
        ...

class Robot:  # No inheritance needed!
    def make_sound(self) -> str:
        return "Beep!"

def hear_sound(thing: SoundMaker) -> None:
    print(thing.make_sound())

hear_sound(Robot())  # ✓ Works without inheritance

python code snippet end

Best Practices

  • Use protocols for defining interfaces in libraries
  • Combine with @runtime_checkable when runtime type checking is needed
  • Keep protocol methods minimal and focused
  • Use generic protocols for reusable interfaces
  • Protocols work great with dependency injection

Protocols bring the best of both worlds: Python’s duck typing flexibility with static type checking safety!

Reference: PEP 544 - Protocols