Protocols & Structural Typing

typing.Protocol gives you interfaces by shape — if it walks like a duck, the type checker agrees.

Python has always had duck typing: if an object has the methods you need, it works — no shared base class required. typing.Protocol brings that idea to the type checker. A Protocol says "anything with these methods counts," and tools like mypy verify it, with zero inheritance.

Structural vs nominal

An ABC is nominal: you must explicitly inherit from it. A Protocol is structural: any class with a matching shape satisfies it automatically, even classes written before your Protocol existed or ones you cannot edit.

from typing import Protocol

class Sized(Protocol):
    def __len__(self) -> int: ...

def describe(obj: Sized) -> str:
    return f'has {len(obj)} items'

print(describe([1, 2, 3]))   # has 3 items — list matches, no import needed
print(describe('hello'))     # has 5 items — str matches too

Runtime checks, optionally

Add @runtime_checkable and you can use isinstance(x, MyProtocol) at runtime (it checks method names exist). Static checking, though, is where Protocols really pay off.

Example

Example · python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Renderable(Protocol):
    def render(self) -> str: ...

# These two classes share NO base class and never heard of Renderable
class Button:
    def __init__(self, label): self.label = label
    def render(self): return f'[ {self.label} ]'

class Heading:
    def __init__(self, text): self.text = text
    def render(self): return f'== {self.text} =='

def paint(widgets: list[Renderable]) -> None:
    for w in widgets:
        print(w.render())

paint([Button('Save'), Heading('Welcome')])

# Structural isinstance check, thanks to @runtime_checkable
print('Button renderable? ', isinstance(Button('x'), Renderable))  # True
print('int renderable?    ', isinstance(42, Renderable))            # False

# Output:
# [ Save ]
# == Welcome ==
# Button renderable?  True
# int renderable?     False

When to use it

  • A logging library accepts any object with a write() method via a Protocol, without requiring inheritance.
  • A sorting utility declares a Comparable Protocol so callers can pass any class that implements __lt__.
  • A test suite uses a Protocol to describe the interface a mock object must satisfy, checked by mypy.

More examples

Define and use a Protocol

Defines a structural Protocol; Invoice satisfies it without inheriting from Printable.

Example · python
from typing import Protocol

class Printable(Protocol):
    def render(self) -> str: ...

class Invoice:
    def render(self) -> str:
        return 'Invoice #1042'

def display(item: Printable) -> None:
    print(item.render())

display(Invoice())   # Invoice #1042

Protocol for duck typing

Accepts any object with a .close() method using structural typing — no explicit inheritance needed.

Example · python
from typing import Protocol

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

def shutdown(resource: Closeable) -> None:
    resource.close()
    print('closed')

import io
shutdown(io.StringIO())   # closed (StringIO has .close())

runtime_checkable Protocol

Makes the Protocol checkable at runtime with isinstance() by adding @runtime_checkable.

Example · python
from typing import Protocol, runtime_checkable

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

class Circle:
    def draw(self): print('Drawing circle')

c = Circle()
print(isinstance(c, Drawable))   # True at runtime
c.draw()

Discussion

  • Be the first to comment on this lesson.