Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 77

TypeVar and Generic for Reusable, Type-Safe Code

Recommended resources

Introduction

In our previous lesson, you learned to design more flexible and robust APIs by choosing the right level of abstraction for your type hints, distinguishing between Iterable and Sequence, and understanding the critical difference between the "trust me" escape hatch Any and the safer, "prove it" approach of object.

So far, our type hints have been for concrete types (int, str, list[str]). But what about functions and classes that are designed to work with many different types, while still maintaining type safety? Think of Python's built-in list or dict. You can have a list[int] or a list[str], and the type system correctly tracks the element type. How can we build our own components with this power?

This lesson answers that question. You will learn to create reusable, type-safe functions and classes using typing.TypeVar and typing.Generic. These are the fundamental tools for building generic components that form the backbone of well-typed, large-scale applications.


The Problem: Losing Type Information

Let's start with a simple function that gets the first item from a list. Without generics, we have two unsatisfying options:

  1. Use a concrete type: def first(items: list[int]) -> int:. This is type-safe, but it only works for lists of integers. We'd need a separate function for lists of strings, floats, etc.
  2. Use Any: def first(items: list) -> Any:. This works for any list, but it throws away type information. mypy will see the return value as Any, forcing the caller to either guess the type or lose type safety downstream.

Generics solve this by allowing us to create a placeholder for a type that is resolved when the function is called.


Generic Functions with TypeVar

The core tool for making functions generic is TypeVar. A type variable is a special variable that acts as a placeholder for a type. When you use a TypeVar in a function signature, you create a contract that links the types of different arguments and the return value.

To see this in action, let's read about the basics of TypeVar.

How to Build Generic Types with TypeVar in Python

This article provides a clear, practical introduction to why generics are needed and how TypeVar solves the problem of lost type information.

Read the sections 'Why Generics Matter', 'TypeVar Basics', 'Naming Conventions for TypeVars', and 'Multiple Type Variables'. Focus on how T = TypeVar('T') creates a placeholder that links the input type to the output type.

As the article explained, by defining T = TypeVar('T') and using it in our first function, we create a link:

from typing import TypeVar, List




# T is a placeholder for any type.
T = TypeVar('T')

def first(items: List[T]) -> T:
    """Return the first element of a list, preserving the element type."""
    return items[0]




# mypy infers that T is 'int' here. The return type is int.
numbers_result: int = first([1, 2, 3])




# mypy infers that T is 'str' here. The return type is str.
strings_result: str = first(["a", "b", "c"])

The type variable T binds the type of the elements in the input list to the function's return type. Whatever type T is for the input, it will be the same for the output.

This GIF provides a great visual of how your IDE and mypy track this relationship in real time.

Generic Function with TypeVar Demonstration
This GIF demonstrates `TypeVar` being used to create a generic `sum` function. Notice how the type checker correctly validates calls with both integers and strings because the types are consistent within each call.

Generic Classes with Generic

Just as with functions, we need a way to make classes generic. This is done by inheriting from typing.Generic and specifying the type variables in square brackets. The classic example is a Stack class.

This video provides an excellent and concise walkthrough of creating a generic Stack.

Python 3.12 Generics in a Nutshell

The video walks through creating a generic Stack class. It also introduces the cleaner syntax for generics available in Python 3.12+, which is good for you to be aware of.

Watch from the beginning up to 03:25. Pay attention to: How class Stack[T] (Python 3.12+) or the older class Stack(Generic[T]) makes the class generic. How the type variable T is used consistently for the internal list (list[T]), the push method's parameter, and the pop method's return type. How instantiating Stack[int] or Stack[float] creates specialized, type-safe versions of the class.

Here is the code for a generic Stack using the syntax compatible with Python versions before 3.12:

from typing import TypeVar, Generic, List, Optional

T = TypeVar('T')

class Stack(Generic[T]):
    """A type-safe stack implementation."""
    def __init__(self) -> None:
        self._items: List[T] = []

    def push(self, item: T) -> None:
        """Add an item to the top of the stack."""
        self._items.append(item)

    def pop(self) -> T:
        """Remove and return the top item."""
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()




# --- Usage ---




# This creates a stack that can ONLY hold integers.
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
value: int = int_stack.pop() # The revealed type is 'int'




# mypy will raise an error on the following line:
# error: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int"
# int_stack.push("a_string")

By inheriting from Generic[T], the Stack class becomes a generic container. The type variable T is scoped to the entire class, ensuring that all its methods operate on the same type.

A Note for Your Java Background

Your experience with Java generics will be helpful here. The concepts are nearly identical, just with different syntax. This table provides a quick translation.

Java Generics vs. Python Type Hint Equivalents
A comparison of common generic type declarations in Java and their Python type hint equivalents. This mapping helps translate your existing knowledge directly to Python.

Building Classes with Multiple Type Parameters

A generic class isn't limited to a single type parameter. Just as you can have Map<K, V> in Java, you can define a Python class with multiple type variables. A Pair class is a good example.

Let's return to the article we read earlier to see how this is implemented.

How to Build Generic Types with TypeVar in Python

Classes can accept multiple, independent type parameters. The 'Pair' class is a classic example. This section demonstrates how to define and use a class with both a key type K and a value type V.

Read the subsections 'The Generic Base Class' and 'Multiple Type Parameters'. Notice how class Pair(Generic[K, V]): is defined and how the type variables K and V are used independently throughout its methods.

This ability to handle multiple, unrelated generic types allows you to model complex data structures like dictionaries, typed key-value stores, or result types that can hold either a success value or an error object of different types.


Conclusion

You now have the tools to move beyond hinting concrete types and can build your own reusable, type-safe generic components. This is a significant step toward designing robust and scalable Python applications.

Key Takeaways:

  • TypeVar Creates Placeholders: T = TypeVar('T') defines a placeholder that mypy replaces with a concrete type at the time of a function call or class instantiation.
  • Generic Functions Use TypeVar: By using a TypeVar in a function's arguments and return type, you create a contract that preserves type relationships (e.g., a function that takes a List[T] returns a T).
  • Generic Classes Inherit from Generic: A class like class MyContainer(Generic[T, V]): becomes a generic type that can be parameterized (e.g., MyContainer[str, int]).
  • Consistency is Key: Within its scope (a function or a class), a TypeVar represents a single, consistent type.

In our final lesson of this module, we will shift our focus from static analysis to the runtime. While we've concentrated on what the type checker can do before your code runs, sometimes you need to make decisions based on an object's type at runtime. We'll cover how to do this safely and Pythonically using isinstance() and issubclass(), and discuss when it's appropriate versus when you should rely on static types or protocols.