Python uses dynamic duck typing by default since the bytecode is interpreted on-the-fly by the python virtual machine.
Later versions of python since 3.8 provides a mechanism known as a Protocol. A protocol specifies the methods and attributes a class must implement in order to be considered a given type. Implementing a protocol is also known as static duck typing.
There are 2 main ways python determines at runtime the type of an object:
Nominal subtyping is based on inheritance. A class which inherits from a parent class is a subtype of its parent.
Structural subtyping is based on the internal structure of classes. A class that implements the methods and attributes of a protocol is a type of the protocol.
The idea is similar to the use of interfaces in go-lang. When we implement all the methods defined by an interface in go-lang, we can use the custom implementation interchangeably where the interface is defined.
Suppose we have an e-commerce platform whereby we want to define and be able to apply different promotions upon cart checkout. We could create a protocol called Promotion that’s callable as a function:
From above, we defined the protocol to be a callable via the call method, which accepts as input an Order and returns a Decimal. Any function that implements this function signature will be considered via structural subtyping to be a type of Promotion.
We can define our promotions as follows:
We define a global list of promotions in promos which specifies via typing that it only accepts a list of promotions. The best_promo function takes an order and applies each promotion in the global list to it in turn and returns the max discount applicable to the order.
Run-time type checking such as isinstance can be enabled by defining runtime_checkable decorator via typing module:
We can now compare that the functions added to the promos global are indeed instances of Promotion:
The full example code for this article is as follows. We use dataclasses to define the Customer, Order and LineItem classes
By using Protocols, we managed to decouple the promotion functions from the codebase. If this were defined using inheritance via abstract base classes, we would need to create a custom class for each promotion, thereby creating a hierachy of inheritance. In this case, there is no clear relationship between the orders and promotions apart from during the checkout process and since promotions can change over time, using protocols allow us to decouple the implementation of the promotions away from the underlying base classes. To remove a promotion, we just remove it from the promos list.
In addition, we can utilise python’s type hints and external type checkers such as mypy. For the example above, we could run mypy as so:
In this post, I aim to explain what python protocols are at a high level and provide a simple implementation of its usage. Future posts will attempt to highlight more advanced use cases of Protocol.