Static Duck Typing with Python Protocols

28-01-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