Summary: in this tutorial, you’ll learn about the Python Protocol and its use to define implicit interfaces.
Introduction to the Python Protocol
Suppose you have a function that calculates the total value of a product list, where each product has the name, quantity, and price attributes:
from typing import List
class Product:
def __init__(self, name: str, quantity: float, price: float):
self.name = name
self.quantity = quantity
self.price = price
def calculate_total(items: List[Product]) -> float:
return sum([item.quantity * item.price for item in items])
Code language: Python (python)
In this example, the calculate_total()
function accepts a list of Product
objects and returns the total value.
When writing this function, you may want to calculate the total of a product list. But you likely want to use it for other lists such as inventory lists in the future.
If you look closely at the calculate_total()
function, it only uses the quantity and price attributes.
To make the calculate_total() more dynamic while leveraging type hints, you can use the Protocol
from the typing
module. The Protocol class has been available since Python 3.8, described in PEP 544.
The following describes how to use the Protocol
class.
First, define an Item
class that inherits from the Protocol
with two attributes: quantity and price:
class Item(Protocol):
quantity: float
price: float
Code language: Python (python)
Second, change the calculate_total()
function that accepts a list of Item
objects instead of a list of Product
objects:
def calculate_total(items: List[Item]) -> float:
return sum([item.quantity * item.price for item in items])
Code language: Python (python)
By doing this, you can pass any list of Item
objects to the calculate_total()
function with the condition that each item has two attributes quantity and price.
The following shows a complete program:
from typing import List, Protocol
class Item(Protocol):
quantity: float
price: float
class Product:
def __init__(self, name: str, quantity: float, price: float):
self.name = name
self.quantity = quantity
self.price = price
def calculate_total(items: List[Item]) -> float:
return sum([item.quantity * item.price for item in items])
# calculate total a product list
total = calculate_total([
Product('A', 10, 150),
Product('B', 5, 250)
])
print(total)
Code language: Python (python)
For example, you can define a list of stocks in inventory and pass them to the calculate_total()
function:
# ...
class Stock:
def __init__(self, product_name, quantity, price):
self.product_name = product_name
self.quantity = quantity
self.price = price
# calculate total an inventory list
total = calculate_total([
Stock('Tablet', 5, 950),
Stock('Laptop', 10, 850)
])
print(total)
Code language: Python (python)
In this example, the Product
and Stock
class don’t need to subclass the Item
class but still can be used in the calculate_total()
function.
This is called duck typing in Python. In duck typing, the behaviors and properties of an object determine the object type, not the explicit type of the object.
For example, an object with the quantity and price will follow the Item
protocol, regardless of its explicit type.
The duck typing is inspired by the duck test:
If it walks like a duck and its quacks like a duck, then it must be a duck.
In practice, when you write a function that accepts input, you care more about the behaviors and properties of the input, not its explicit type.
Summary
- Use Python Protocol to define implicit interfaces.