Structural Subtyping with Python Protocols
January 16, 2022
Overview
In Python, a protocol is a class that is used to define an interface (a set of attributes and methods) and check that an object implicitly implements this interface. A Protocol
class may be thought of as an implicit abstract base class that allows for structural subtyping / static duck-typing. Classes that conform to the attributes and methods of this protocol are considered to be a subclass of this protocol for the purpose of type checking.
Duck Typing
Duck typing is an idiomatic practice whereby type checking is resolved based on the expected behavior of an object. Behavior is defined by an objects' attributes and methods and expectations are defined by those who call an objects' attributes and methods. This follows Python's convention of Easier to Ask for Forgiveness than Permission.
An example of duck typing is an iteration, whereby a variety of objects (lists, arrays, generators etc) are able to be iterated over using a simple for loop. These iterable objects are not strongly type-checked, but rather just need to implement the __iter__()
and __next__()
functions.
Under the Hood
Protocol classes are used as type annotations to help static type checkers enforce the expected behavior of an interface.
- All attributes and methods of a protocol class have to be implemented in each of the target classes in order to conform to the protocol
- At runtime, protocol classes are created as instances of
abc.ABCMeta
- Protocols cannot be instantiated
- No runtime checks are imposed
- You may define aggregate protocol classes
Protocols vs Abstract Classes
Protocols help maintain idiomatic patterns in Python within a typed setting. Protocols provide a functionality similar to abstract classes, however, in contrast, support such behavior statically (not at runtime). As such, you do not need to explicitly subclass or register a protocol to use it. This provides greater dynamic flexibility, saves on runtime costs and prevents you from worrying about sub-classing abstract classes hidden deep within the codebase.
Usage
We may illustrate the usage of protocols by creating a simple protocol class to represent a software engineer, and two concrete classes representing a data engineer and a blockchain engineer.
from typing import Protocol
class SoftwareEngineer(Protocol):
def write_clean_code(self):
...
class DataEngineer:
def write_clean_code(self):
...
class BlockchainEngineer:
def write_spaghetti_code(self):
...
class Application:
def write_code(self, obj: SoftwareEngineer): # Use protocol as type check
return obj.write_clean_code()
def main():
app = Application()
app.write_code(DataEngineer()) # ✅ Passes type check (implements behavior of protocol)
app.write_code(BlockchainEngineer()) # ❌ Fails type check (does not implement behavior of protocol)
if __name__ == '__main__':
main()
Further Reading
- PEP 544 -- Protocols: Structural subtyping (static duck typing)
- structural subtyping is natural for Python programmers since it matches the runtime semantics of duck typing: an object that has certain properties is treated independently of its actual runtime class
- typing module defines various protocols
- class must be explicitly marked to support them
- unpythonic / not idiomatic dynamically typed Python code
- abstract classes / ABCs
- class must be explicitly subclassed or registered
- difficult to do with (external) library types as the type objects may be hidden deep in the implementation of the library
- allows you to control the definition of the interface
- may impose additional runtime costs