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 tooRuntime 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
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? FalseWhen 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.
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 #1042Protocol for duck typing
Accepts any object with a .close() method using structural typing — no explicit inheritance needed.
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.
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