Skip to Content
Course content

308: Generics with TypeVar

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're designing a high-end shipping crate. This crate is engineered to protect whatever is inside it, whether it's a delicate glass vase, a heavy engine part, or a stack of rare books. The crate doesn't need to know the specific properties of a vase to keep it safe; it just needs to be a "container for something." Crucially, if you put a vase in, you expect to get a vase back out—not a random object that might or might not be a vase.

In Python, we often run into this exact scenario. You want to write a function that works with any data type, but you want the type checker to remember which specific type was used. If you just use Any, you're telling Python, "I don't care what this is," and you lose all the helpful autocomplete and safety checks. That's where TypeVar comes in. It's our way of saying, "I don't know what the type is yet, but whatever it is, it stays consistent throughout this function."

The frustration of losing type identity

Let's look at a common pattern: a function that returns the first item of a list. If I write it using Any, I'm throwing away valuable information.

from typing import Any, List

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

names = ["Alice", "Bob", "Charlie"]
first_name = get_first(names) 
# The IDE thinks first_name is 'Any'. 
# I lose autocomplete for string methods like .upper()

I've seen a lot of developers just settle for Any here because it's easy. But as your codebase grows, that "Any" becomes a black hole. You'll find yourself constantly casting types or, worse, hitting AttributeError at runtime because you forgot what the function actually returned.

Creating a type placeholder

To fix this, we use TypeVar. Think of it as a variable for a type. By convention, we usually call it T, but you can call it whatever you like.

from typing import TypeVar, List

# We define T as a placeholder for "some type"
T = TypeVar('T')

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

names = ["Alice", "Bob", "Charlie"]
first_name = get_first(names) 
# Now, Python knows that because 'names' is a List[str], 
# T must be 'str'. Therefore, first_name is a 'str'.

Notice what happened there. I didn't tell Python that T is a string. I told Python: "Whatever type the list contains, the return value will be that exact same type." It's a relationship, not a definition.

Putting guardrails on your generics

Sometimes, you don't want any type. You might want a function that works with any type, as long as it's a number. If you try to use a string in a math-heavy generic function, your code will crash. We can handle this by "constraining" the TypeVar.

You do this by passing the allowed types as arguments to the TypeVar constructor. Here's how I usually handle functions that need to support both integers and floats but nothing else:

from typing import TypeVar

# T can only be an int or a float
T = TypeVar('T', int, float)

def add_bonus(value: T, bonus: T) -> T:
    return value + bonus

print(add_bonus(10, 5))      # Works! (int)
print(add_bonus(10.5, 2.5))  # Works! (float)
# add_bonus("10", "5")       # Static type checker will scream at you here

I love this approach because it's explicit. You're telling other developers (and your future self) exactly what the boundaries are. You get the flexibility of generics without the "wild west" feel of Any.




📋 Practical Task

Build a Generic Response Wrapper

In professional API development, we often wrap our data in a consistent "Response" object that includes metadata (like a success flag) and the actual payload. The payload could be a User object, a list of Products, or a simple Error string.

Your task is to create a generic class called ApiResponse using TypeVar. It should:

  1. Define a TypeVar called T.
  2. Have an __init__ method that accepts data of type T and status as a string.
  3. Have a method called get_data that returns the data, ensuring the return type is mapped to T.

Test your class by creating one instance with a list of strings and another with a dictionary. Verify (using a type checker or by observing IDE behavior) that the return type of get_data() changes based on what you passed into the constructor.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.