Python Protocols and Structural Subtyping
Structural Subtyping Core
Structure Over Name
In object-oriented programming, we often think about types in terms of family trees. A GoldenRetriever class is a subtype of a Dog class because it explicitly inherits from it. This is called nominal subtyping, because the relationship is based on the name of the parent class in the definition.
Python's dynamic nature has always embraced a more flexible idea: "If it walks like a duck and quacks like a duck, then it must be a duck." This is often called duck typing. The actual class name or its inheritance history doesn't matter, only whether it has the right methods and attributes for a specific task.
Structural subtyping formalizes this concept for static type checkers like Mypy. It defines a type relationship based on an object's structure—its methods and attributes—rather than its declared inheritance. The typing.Protocol class is the tool that lets us define these structural contracts.
With nominal subtyping, you ask, "What is your family?" With structural subtyping, you ask, "What can you do?"
Defining a Protocol
A Protocol is a class that inherits from typing.Protocol. It looks like a standard abstract base class (ABC), but with a key difference: other classes don't need to inherit from it to be considered one of its subtypes. They just need to match its structure.
Inside a Protocol, you define the methods and attributes that any conforming class must have. For methods, you include the type signature but use an ellipsis (...) for the body. For attributes, you simply declare them with their type.
from typing import Protocol
class SupportsQuacking(Protocol):
# This is an attribute requirement
volume_db: int
# This is a method requirement
def quack(self) -> str:
...
Any class that has an integer attribute named volume_db and a method named quack that takes no arguments (besides self) and returns a string will implicitly be considered a subtype of SupportsQuacking by a static type checker. It doesn't need to say class MyDuck(SupportsQuacking):.
Implicit Conformance in Action
Let's see how this works. We'll define two completely unrelated classes, Duck and RoboticBird. Neither inherits from SupportsQuacking, but both happen to fit its structure.
class Duck:
def __init__(self, loudness: int):
self.volume_db = loudness
def quack(self) -> str:
return "Quack!"
class RoboticBird:
def __init__(self, volume_level: int):
self.volume_db = volume_level * 10 # Convert level to dB
def quack(self) -> str:
return "BEEP-QUACK-BOOP"
# This class does NOT conform
class Dog:
def bark(self) -> str:
return "Woof!"
Now, we can write a function that expects an object matching our protocol. Both Duck and RoboticBird instances are valid arguments, but a Dog instance is not. A static type checker will catch this error before you even run the code.
def make_it_quack(thing: SupportsQuacking):
# The type checker knows `thing` has these properties
# because it conforms to the Protocol.
print(f"Sound: {thing.quack()} at {thing.volume_db} dB")
daffy = Duck(loudness=85)
mecaduck = RoboticBird(volume_level=9)
fido = Dog()
# --- These calls are valid ---
make_it_quack(daffy)
make_it_quack(mecaduck)
# --- This call is invalid and a type checker will flag it ---
# Mypy Error: Argument 1 to "make_it_quack" has incompatible type "Dog";
# expected "SupportsQuacking"
# Note: "Dog" is missing attribute "quack"
# Note: "Dog" is missing attribute "volume_db"
make_it_quack(fido)
This approach allows you to build decoupled systems. The make_it_quack function doesn't need to know anything about Duck or RoboticBird classes. It only cares that the object it receives can perform a certain action (quack) and has a certain property (volume_db). This makes your code more flexible and easier to test, as you can easily create mock objects for tests that conform to your protocols.
What is the key difference between nominal subtyping and structural subtyping?
How must a class relate to a typing.Protocol to be considered a subtype by a static type checker?
Protocols offer a powerful way to write flexible, type-safe Python code by focusing on what objects can do rather than what they are.