Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
309: Defining Custom Generic Classes
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
TypeVarnamedT. - Create a class
PaginatedResultthat inherits fromGeneric[T]. - The
__init__method should acceptitems: list[T],page: int, andtotal_pages: int. - Implement a method
get_first_item() -> T | Nonethat returns the first item of the list if it exists, otherwiseNone. - 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 thatget_first_item()returns the correct type for each.
There are no comments for now.