No history yet

Generics Fundamentals

Beyond Basic Types

You're already familiar with type hints like int, str, and even more complex ones like Union[str, int] or Optional[str]. These are great for describing fixed types. But what happens when you write a function that's designed to work with any type, while still maintaining the connection between its inputs and outputs?

Consider a simple function that returns the first item in a list. How would you type hint it? You could use Any, but that sacrifices type safety. A static type checker like Mypy would lose all information about the returned value.

from typing import Any, List

def get_first(items: List[Any]) -> Any:
    return items[0]

numbers = [1, 2, 3]
first_num = get_first(numbers)

# Mypy has no idea that `first_num` is an `int`.
# It only knows it's `Any`, so it can't catch errors.
first_num.upper() # This will crash at runtime, but Mypy won't complain.

This is where generics come in. They allow us to create components that are flexible enough to work with various types but rigid enough to enforce type consistency. We can tell the type checker, "Hey, this function takes a list of some type, and it returns a value of that exact same type."

Introducing TypeVar

The core building block for generics in Python is TypeVar. A TypeVar is essentially a placeholder for a type. Think of it as a variable, but for types instead of values.

You create a TypeVar instance to represent this unknown-yet-consistent type.

from typing import TypeVar

# By convention, type variables are single capital letters.
T = TypeVar('T')

# You can also use more descriptive names.
ItemType = TypeVar('ItemType')

When a type checker sees a TypeVar like T, it doesn't know what T will be. It could be int, str, or a custom class. But it knows that every time it sees T within the same context (like a function signature), it must refer to the same type. This is the key to maintaining type relationships.

TypeVar creates a link. It tells the type checker that the type of one thing is the same as the type of another, without needing to know what that specific type is ahead of time.

Generic Functions

Let's fix our get_first function using TypeVar. By replacing Any with our type variable T, we create a generic function. This tells the type checker that the input list contains elements of type T, and the function returns a single value of that same type T.

from typing import List, TypeVar

# Create a type variable
T = TypeVar('T')

def get_first(items: List[T]) -> T:
    return items[0]

# Now, the type checker understands the relationship.
numbers = [1, 2, 3]
first_num = get_first(numbers) # Mypy infers first_num is an int

strings = ["a", "b", "c"]
first_str = get_first(strings) # Mypy infers first_str is a str

# This now correctly produces a type error!
first_num.upper()

When we call get_first([1, 2, 3]), the type checker sees that the list contains integers. It binds the type int to our placeholder T. Therefore, it knows the function signature effectively becomes def get_first(items: List[int]) -> int, and the return value must be an int.

This is a huge improvement over Any. Instead of erasing type information, we've created a flexible function that preserves it.

Simple Generic Classes

The same principle applies to classes. Sometimes you want to create a container class that can hold any type of data, like a box for a single item. To make a class generic, you have it inherit from Generic from the typing module and use the TypeVar in square brackets.

from typing import TypeVar, Generic

# Define a TypeVar to use as a placeholder for the content type
T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, content: T):
        self.content = content

    def get_content(self) -> T:
        return self.content

# Usage:
box_of_int = Box(123)
box_of_str = Box("hello")

# The type checker knows the exact type of the content
value_from_int_box: int = box_of_int.get_content()
value_from_str_box: str = box_of_str.get_content()

# This will correctly raise a type error
value_from_str_box.bit_length()

By defining class Box(Generic[T]), we are telling the type system that Box is a generic container. The type of item it holds is represented by T. When we create an instance like Box(123), the type checker infers that T is int for this specific instance. So, box_of_int is of type Box[int], and its get_content method is guaranteed to return an int.

Generics provide the perfect balance: code that is reusable across different types while also being fully type-safe.

Time to check your understanding.

Quiz Questions 1/4

What is the primary advantage of using a TypeVar over Any in a function signature?

Quiz Questions 2/4

Which line of code correctly creates a generic class Container that can hold any type of item?

You now have the foundation for writing flexible, robust, and type-safe Python code using generics.