Skip to Content
Course content

309: Defining Custom Generic Classes

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

I was working on a project recently where I needed a way to wrap API responses. Some responses returned a single User object, others returned a list of Product objects, and some just returned a boolean. I wanted a consistent Response wrapper that tracked things like the timestamp and the status code, regardless of what the actual data was.

The "Any" Trap

My first instinct was to just use Any. It's the easy way out, right? Let's look at how I started this:

from typing import Any
from datetime import datetime

class Response:
    def __init__(self, data: Any, status: int):
        self.data = data
        self.status = status
        self.timestamp = datetime.now()

    def get_data(self) -> Any:
        return self.data

# Using it
user_resp = Response({"name": "Alice"}, 200)
data = user_resp.get_data() 
# My IDE has no idea what 'data' is here. It's just 'Any'.

This works at runtime, but it's a nightmare for maintenance. If I'm calling get_data(), I want my IDE to tell me if I'm accessing a field that doesn't exist. With Any, the type checker just shrugs its shoulders. I'm essentially flying blind.

Attempting to use TypeVars without the Glue

I remember seeing TypeVar in the docs, so I thought I'd just throw one in there to "label" the type. I tried this:

from typing import TypeVar, Any

T = TypeVar('T')

class Response:
    def __init__(self, data: T, status: int):
        self.data = data
        self.status = status

    def get_data(self) -> T:
        return self.data

I figured, "Okay, T represents the type of the data." But when I actually tried to use it, I realized I had a problem. If I create a Response, Python doesn't actually "link" the T to a specific type for that specific instance. It's still treating T as a generic placeholder, not a concrete type for that object. It's better than Any, but it's still not giving me the precise autocomplete I want when I instantiate the class.

Connecting it with Generic

Here is the "aha!" moment. To make a class actually behave as a generic template, it has to inherit from Generic. This tells Python (and the type checker) that the class is designed to be parameterized.

from typing import TypeVar, Generic
from datetime import datetime

T = TypeVar('T')

class Response(Generic[T]):
    def __init__(self, data: T, status: int):
        self.data = data
        self.status = status
        self.timestamp = datetime.now()

    def get_data(self) -> T:
        return self.data

By adding Generic[T] to the base class, I've turned Response into a factory for other types. Now, when I instantiate it, I can explicitly tell Python what T is for this specific instance.

Locking in the Type

Now let's see the payoff. Watch what happens when we specify the type during instantiation:

class User:
    def __init__(self, name: str):
        self.name = name

# I'm telling Python: "This is a Response that contains a User"
user_resp = Response[User](User("Alice"), 200)

# Now, when I call get_data(), the IDE knows exactly what it is
user = user_resp.get_data()
print(user.name) # Autocomplete works perfectly here!

# I can do the same for a list of strings
list_resp = Response[list[str]](["Apple", "Banana"], 200)
items = list_resp.get_data()
# items is recognized as list[str]

This is the real power of custom generic classes. I'm not just saying "this holds something"; I'm saying "this holds a specific type, and that type should persist across all methods in this class." It keeps my code dry—I only wrote the Response logic once—but I get full type safety for every different kind of data I wrap.




📋 Practical Task

Build a Generic PaginatedResult Wrapper

You are building a system that handles large datasets. To avoid loading everything at once, you need a PaginatedResult class that wraps a list of items and provides metadata about the pagination (current page and total pages).

Your task:

  • Define a TypeVar named T.
  • Create a class PaginatedResult that inherits from Generic[T].
  • The __init__ method should accept items: list[T], page: int, and total_pages: int.
  • Implement a method get_first_item() -> T | None that returns the first item of the list if it exists, otherwise None.
  • Instantiate the class twice: once for a list of integers (e.g., PaginatedResult[int]) and once for a list of strings (e.g., PaginatedResult[str]), and verify that get_first_item() returns the correct type for each.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.