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

Try it yourself
Loading editor…
Press Run to execute the code.

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.