backend / python core / typing / 03_protocol_structural_typing.md

Protocol and structural typing

2 min read source

Protocol and structural typing

Protocol (PEP 544) gives Python static duck typing — types that match by shape (methods/attributes) rather than by inheritance.

The problem

Without protocols, expressing “anything with a read() method” requires either an ABC (which classes must explicitly inherit from) or Any:

from typing import IO   # too restrictive — what if it's a custom class?

def consume(f: IO) -> str: ...

With a Protocol, any class that happens to have the right methods qualifies:

from typing import Protocol

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

def consume(f: Readable) -> str:
    return f.read()

class MyReader:
    def read(self, n: int = -1) -> str:
        return "data"

consume(MyReader())   # MyReader matches Readable structurally
consume(open("x.txt"))  # file objects also match

MyReader doesn’t inherit from Readable. It just has the right method shape.

runtime_checkable

By default, isinstance(x, MyProtocol) raises. Add the decorator if you need it:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Closeable(Protocol):
    def close(self) -> None: ...

if isinstance(obj, Closeable):
    obj.close()

Caveats: runtime check only verifies method names exist, not signatures or types. It’s slow (uses hasattr).

Common standard protocols

from typing import Iterable, Iterator, Sized, Container, Hashable

# These are already Protocols (or ABCs that act like ones)
class Sized(Protocol):
    def __len__(self) -> int: ...

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

So def f(x: Sized) -> int: return len(x) accepts anything with __len__.

Protocols vs ABCs

Aspect ABC Protocol
Subclass declaration required (class Foo(ABC):) none — structural
Compile-time only? No, ABCs check at instantiation Yes (unless runtime_checkable)
Method enforcement enforced when subclass is instantiated only checked statically
Mixing with third-party types requires registering works automatically

Use Protocols when you want to type-check interfaces without forcing inheritance. Use ABCs when you want runtime contract enforcement and shared default methods.

Generic protocols

Protocols can be generic:

from typing import Protocol, TypeVar

T = TypeVar("T")

class Container(Protocol[T]):
    def __contains__(self, item: T) -> bool: ...

def has_zero(c: Container[int]) -> bool:
    return 0 in c

Interview angle

“What’s the difference between an ABC and a Protocol?” (ABC = nominal subtyping, Protocol = structural / duck typing.) “When would you use each?” (Protocol for typing existing third-party objects without modifying them; ABC when you want shared implementation + runtime enforcement.)