Python Protocol

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: floatCode 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.
Did you find this tutorial helpful ?