Protocol Classes: Structural Typing Guide
Protocols in Python allow you to define an implicit interface—a set of methods and attributes—that any class can satisfy without explicit inheritance. Instead of saying def process(obj: BaseClass), you say def process(obj: Drawable) and accept any object with a draw() method, even if it never inherited from a Drawable base class. This is called structural typing and is a powerful pattern for flexible, type-safe Python code.
What Are Protocols?
A Protocol is a type that specifies a set of methods and attributes. Unlike a base class, a class does not need to inherit from a Protocol to satisfy it—it only needs to have the right methods with the right signatures. This is duck typing with type checking.
Here's an example:
from typing import Protocol
class Drawable(Protocol):
"""Anything with a draw() method."""
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing a circle")
class Square:
def draw(self) -> None:
print("Drawing a square")
def render(shape: Drawable) -> None:
"""Draw any Drawable object."""
shape.draw()
# Both Circle and Square satisfy Drawable without inheriting from it
render(Circle()) # OK: Circle has draw() method
render(Square()) # OK: Square has draw() method
Neither Circle nor Square inherits from Drawable, yet mypy accepts them as Drawable objects because they have the required draw() method. This is structural typing: the structure of the object (what methods it has) matters, not its declared type hierarchy.
Defining Protocols with Methods
A Protocol definition looks like a class, but uses ... (ellipsis) for method bodies:
from typing import Protocol
class Comparable(Protocol):
"""Anything that can be compared for less-than ordering."""
def __lt__(self, other: "Comparable") -> bool: ...
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __lt__(self, other: "Person") -> bool:
return self.age < other.age
def find_youngest(people: list[Comparable]) -> Comparable:
"""Find the smallest element using __lt__."""
return min(people)
# Even though Person doesn't inherit from Comparable,
# it satisfies the protocol because it has __lt__()
people = [Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35)]
youngest = find_youngest(people) # mypy: OK
Protocols with Properties
Protocols can specify properties (attributes accessed without calling):
from typing import Protocol
class Nameable(Protocol):
"""Anything with a name attribute."""
@property
def name(self) -> str: ...
class Person:
def __init__(self, full_name: str):
self._full_name = full_name
@property
def name(self) -> str:
return self._full_name
class Dog:
def __init__(self, breed: str):
self.name = breed
def greet(entity: Nameable) -> None:
print(f"Hello, {entity.name}!")
greet(Person("Alice")) # OK: Person.name is a property
greet(Dog("Labrador")) # OK: Dog.name is a regular attribute
Both properties and regular attributes satisfy the protocol—mypy doesn't distinguish between them structurally.
Runtime Checking with runtime_checkable
By default, Protocol checking is compile-time only (mypy). To check at runtime, use the @runtime_checkable decorator:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("Drawing")
circle = Circle()
print(isinstance(circle, Drawable)) # True: Circle has draw()
# Check without isinstance
if hasattr(circle, 'draw') and callable(getattr(circle, 'draw')):
print("It's drawable!") # Always true
With @runtime_checkable, you can use isinstance() to check if an object satisfies a Protocol at runtime—useful for defensive coding and dynamic dispatch.
Generic Protocols with TypeVar
Protocols can be generic (parameterized by type variables):
from typing import Protocol, TypeVar, Generic
T = TypeVar("T")
class Container(Protocol[T]):
"""Anything that holds items of type T."""
def add(self, item: T) -> None: ...
def get(self) -> T: ...
class Stack:
def __init__(self) -> None:
self.items: list[object] = []
def add(self, item: object) -> None:
self.items.append(item)
def get(self) -> object:
return self.items.pop() if self.items else None
def process_container(container: Container[int]) -> None:
container.add(42)
value: int = container.get()
# Stack satisfies Container, so can be used where Container[int] is needed
stack: Stack = Stack()
process_container(stack) # mypy may complain; Stack is Container[object], not Container[int]
Protocols with type parameters are most useful when you need to enforce type consistency across multiple methods.
Practical Example: Plugin System with Protocols
Here's a real-world example—a plugin system using Protocols:
from typing import Protocol, runtime_checkable, List
@runtime_checkable
class Plugin(Protocol):
"""Any plugin must have a name and a run() method."""
@property
def name(self) -> str: ...
def run(self, data: dict) -> dict: ...
class UppercasePlugin:
"""Convert all strings in data to uppercase."""
@property
def name(self) -> str:
return "uppercase"
def run(self, data: dict) -> dict:
return {k: v.upper() if isinstance(v, str) else v for k, v in data.items()}
class LogPlugin:
"""Log the data and return it unchanged."""
@property
def name(self) -> str:
return "log"
def run(self, data: dict) -> dict:
print(f"Processing: {data}")
return data
class PluginRunner:
def __init__(self) -> None:
self.plugins: List[Plugin] = []
def register(self, plugin: Plugin) -> None:
"""Register a plugin without requiring it to inherit from a base class."""
self.plugins.append(plugin)
def run(self, data: dict) -> dict:
"""Run all registered plugins in sequence."""
result = data
for plugin in self.plugins:
result = plugin.run(result)
return result
# Use the plugin system
runner = PluginRunner()
runner.register(LogPlugin())
runner.register(UppercasePlugin())
output = runner.run({"greeting": "hello", "count": 42})
# Output: {"greeting": "HELLO", "count": 42}
Neither LogPlugin nor UppercasePlugin inherits from Plugin or any base class, yet they both satisfy the Plugin Protocol. This flexibility is the power of structural typing.
Protocols vs. Abstract Base Classes
Protocols and Abstract Base Classes (ABCs) both enforce structure, but differ:
| Aspect | Protocol | ABC |
|---|---|---|
| Inheritance | Not required; structural | Required (explicit) |
| Runtime checking | @runtime_checkable only | Built-in with isinstance() |
| Flexibility | Highly flexible; any class works | Tighter coupling |
| Performance | Minimal overhead | Slight metaclass overhead |
| Best for | Implicit interfaces, duck typing | Enforcing contracts |
Use Protocols when you want flexible duck-typing-friendly interfaces. Use ABCs when you need explicit contracts and inheritance hierarchies.
Common Pitfall: Protocol Variance
Protocols can be covariant or contravariant in their method parameters—this matters for type safety:
from typing import Protocol
class Consumer(Protocol):
"""Consumes data of type T."""
def consume(self, data: int) -> None: ...
def use_consumer(c: Consumer) -> None:
c.consume(42)
class StringConsumer:
def consume(self, data: object) -> None: # More general parameter
print(data)
use_consumer(StringConsumer()) # mypy: OK (contravariance)
This works because StringConsumer.consume() accepts object, which is more general than int—it can safely consume any int. This is called contravariance and is safe. The opposite (consuming a more specific type) is unsafe and mypy will catch it.
Key Takeaways
- Protocols define implicit interfaces without requiring inheritance
- Use
class MyProtocol(Protocol): ...with...for method bodies - Any class with the right methods satisfies a Protocol, even without inheriting from it
- Add
@runtime_checkableto enableisinstance()checks at runtime - Protocols are generic:
Protocol[T]with type variables for flexible, parameterized interfaces - Protocols are ideal for plugin systems, duck-typing-friendly APIs, and flexible function signatures
- Use Protocols instead of ABCs when you want loose coupling and structural typing
Frequently Asked Questions
Can a class inherit from a Protocol?
Yes, a class can explicitly inherit from a Protocol, but it's not required. Explicit inheritance makes the intent clearer but offers no additional type safety.
Do I need @runtime_checkable for mypy to understand Protocols?
No. Mypy checks Protocols at compile time. @runtime_checkable only enables isinstance() checks at runtime. For compile-time checking alone, the decorator is unnecessary.
Can a Protocol inherit from another Protocol?
Yes: class Extended(Protocol, BaseProtocol): .... The derived Protocol must satisfy all methods from both itself and the base.
How does mypy know if a class satisfies a Protocol?
mypy performs structural checking: it verifies that the class has all the methods and properties the Protocol requires, with compatible signatures. Inheritance is not checked.
What if my class has extra methods not in the Protocol?
That's fine. Protocols only specify a minimum set of methods. Having extra methods doesn't violate the protocol—extra methods are simply ignored.
Further Reading
- Python
typing.ProtocolDocumentation — Official reference and examples - PEP 544: Protocols — The specification for Protocols in Python
- mypy Protocol Guide — mypy-specific Protocol semantics and best practices
- Real Python: Protocols in Python — Practical tutorial and design patterns