Advanced Python Type Hinting: Collections and Structures
Nested Generic Collections
Handling Complex Data
You're comfortable annotating simple collections like list[int] or dict[str, str]. But real-world data is rarely that clean. Often, you'll work with data structures nested inside one another, like lists of dictionaries, which is common when fetching data from a JSON API.
Before Python 3.9, you needed to import List and Dict from the typing module. Now, you can use the built-in list and dict types directly, making the syntax cleaner and more intuitive. Let's look at how to annotate a list where each item is another list of integers.
matrix: list[list[int]] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
The outer list indicates the primary collection, and the inner list[int] specifies that each element inside the outer list is, itself, a list of integers. This nesting can go as deep as needed.
A more practical example involves a list of dictionaries. Imagine you have data representing a list of users, where each user is a dictionary.
users: list[dict[str, int | str]] = [
{"user_id": 101, "username": "alice"},
{"user_id": 102, "username": "bob"}
]
Here, users is a list. Each item in the list is a dictionary (dict) that must have string keys. The values in the dictionary can be either an integer or a string, which we denote with the union type int | str.
Simplifying with Type Aliases
As type hints get more complex, they can become unwieldy and hard to read. Repeating list[dict[str, int | str]] throughout your code is not ideal. To solve this, you can create a type alias—a custom name for a type annotation.
Using a type alias makes your code more readable and easier to maintain. You define the complex type once and reuse the alias everywhere else.
from typing import TypeAlias
# Define a custom type alias for a user dictionary
User = dict[str, int | str]
# Use the alias in your function annotation
def process_users(users: list[User]) -> None:
for user in users:
print(f"Processing user: {user['username']}")
process_users([
{"user_id": 101, "username": "alice"},
{"user_id": 102, "username": "bob"}
])
By defining User as an alias for our dictionary structure, the process_users function signature becomes much clearer. Anyone reading it immediately understands it expects a list of User objects.
Creating Generic Collections
Sometimes you need to write a function that can operate on collections of different types, but you still want to maintain type safety. For example, a function that gets the first item from any non-empty list, no matter what the list contains. This is where TypeVar comes in.
TypeVar
other
A type variable that lets you link several types together, allowing for the creation of generic functions and classes.
A TypeVar acts as a placeholder for a type. When you define a function with a TypeVar, a static type checker understands that whatever type is passed in for one argument must be the same for another argument or the return value.
from typing import TypeVar, Sequence
# Create a TypeVar named 'T'. It can be any type.
T = TypeVar('T')
def get_first(items: Sequence[T]) -> T:
"""Returns the first item of any sequence."""
if not items:
raise ValueError("Sequence cannot be empty")
return items[0]
# Mypy will infer the type correctly
first_num = get_first([1, 2, 3]) # Inferred type of first_num is int
first_str = get_first(("a", "b", "c")) # Inferred type of first_str is str
In this example, T is our type variable. We've defined get_first to accept a Sequence[T] (a generic way to represent lists, tuples, etc.) and return a value of type T. When we call get_first([1, 2, 3]), the type checker sees that T is int, so it knows the function will return an int. Similarly, for get_first(("a", "b", "c")), T becomes str.
Now, let's check your understanding of these concepts.
How would you correctly annotate a variable user_data that holds a list of dictionaries, where each dictionary represents a user with a string name and an integer id?
What is the primary benefit of creating a type alias like User = dict[str, int | str]?
Mastering nested type hints allows you to accurately describe complex data structures, making your code safer and easier for others to understand.