Mypy: define function working on every children of a TypedDict

2 min read 05-10-2024
Mypy: define function working on every children of a TypedDict


Mypy Mastery: Typing Functions That Work with TypedDict Children

Problem: You're working with a TypedDict in Python and need to define a function that operates on each of its values. However, Mypy, your trusty type checker, throws errors because it can't understand how your function interacts with the specific types of the TypedDict's children.

Rephrased: Imagine you have a box filled with different objects: apples, oranges, and pears. You want to write a function that can wash all the fruits in the box. Mypy, the inspector, wants to know exactly what type of fruit you'll be washing in each case.

Scenario:

Let's say you have a TypedDict representing a customer's order:

from typing import TypedDict

class Order(TypedDict):
    item: str
    quantity: int
    price: float

order: Order = {
    "item": "Apple",
    "quantity": 5,
    "price": 1.25
}

You want to create a function apply_discount that applies a percentage discount to the price of each item in the order.

def apply_discount(order: Order, discount: float) -> None:
    for key, value in order.items():
        if key == "price":
            order[key] = value * (1 - discount)

Mypy will complain here because it doesn't know that order[key] will always be a float when key is "price."

Solution:

To solve this, we can use the typing.Any type or generic types. However, both solutions introduce type ambiguity and limit Mypy's effectiveness.

The Best Approach: Use TypeVar and Generic Types

  1. Define a generic type:

    from typing import Generic, TypeVar, TypedDict
    
    T = TypeVar('T') 
    
    class Order(TypedDict):
        item: str
        quantity: int
        price: float 
    
  2. Create a generic function:

    from typing import Generic, TypeVar, TypedDict
    
    T = TypeVar('T') 
    
    class Order(TypedDict):
        item: str
        quantity: int
        price: float 
    
    def apply_discount(order: Order, discount: float) -> None:
        for key, value in order.items():
            if isinstance(value, float): 
                order[key] = value * (1 - discount)
    

Explanation:

  • TypeVar('T'): This creates a type variable T representing any type.
  • Generic[T]: We make our function apply_discount a generic function, allowing it to work with any type.
  • isinstance(value, float): This check ensures we only apply the discount to values that are floats (in our case, the "price" value).

**Mypy will now be happy because it knows the function operates on a specific type. This approach allows for more flexibility and type safety without sacrificing Mypy's benefits. **

Additional Tips:

  • Use typing.Union to handle situations where a key could have multiple types:

    class Order(TypedDict):
        item: str
        quantity: int
        price: Union[float, int] 
    
  • For more complex operations, consider creating specialized functions for each TypedDict field. This promotes code organization and clarity.

Resources:

By leveraging generic types and TypeVar, you can write type-safe functions that work seamlessly with TypedDict children, ensuring your code is both readable and reliable.