Skip to content

TypeVar and Generic

TypeVar defines type variables for generic functions and classes, allowing flexible yet type-safe code.

Mental Model

A TypeVar is a placeholder for "some type to be determined later." When you write def first(items: list[T]) -> T, the T binds to whatever type the caller passes — list[int] makes it int, list[str] makes it str. This lets you write one function that is generic yet fully type-checked, without resorting to Any.

TypeVar - Generic Type Variables

Use TypeVar to write generic functions that work with any type.

```python from typing import TypeVar

T = TypeVar('T')

def first(items: list[T]) -> T: return items[0]

def last(items: list[T]) -> T: return items[-1]

nums = [1, 2, 3] strs = ['a', 'b', 'c']

print(first(nums)) # int print(last(strs)) # str ```

1 c

Constrained TypeVar

Restrict TypeVar to specific types.

```python from typing import TypeVar

Only int or str

T = TypeVar('T', int, str)

def process(value: T) -> T: if isinstance(value, int): return value * 2 # type: ignore else: return value.upper() # type: ignore

print(process(5)) print(process("hello")) ```

10 HELLO

Generic Classes

Create generic classes with type parameters.

```python from typing import Generic, TypeVar

T = TypeVar('T')

class Container(Generic[T]): def init(self, item: T): self.item = item

def get(self) -> T:
    return self.item

int_container = Containerint str_container = Containerstr

print(int_container.get()) print(str_container.get()) ```

42 hello


Runnable Example: generics_tutorial.py

```python """ Tutorial 07: Generic Types and TypeVar ======================================

Level: Advanced

This tutorial covers generic types, TypeVar, and how to create flexible, reusable functions and classes that work with multiple types.

Learning Objectives: - Understand and use TypeVar - Create generic functions - Build generic classes - Use constraints and bounds on TypeVars - Work with multiple TypeVars

Prerequisites: - All previous tutorials """

from typing import TypeVar, Generic, List, Optional, Sequence, Callable

=============================================================================

SECTION 1: TypeVar Basics

=============================================================================

TypeVar creates a type variable that can represent any type

T = TypeVar('T')

def identity(value: T) -> T: """Return the same value - preserves type.""" return value

Usage:

x: int = identity(5) # T is int y: str = identity("hello") # T is str

def get_first(items: Sequence[T]) -> Optional[T]: """Get first item, preserving type.""" return items[0] if items else None

def reverse_list(items: List[T]) -> List[T]: """Reverse a list, preserving element type.""" return list(reversed(items))

=============================================================================

SECTION 2: Multiple TypeVars

=============================================================================

T1 = TypeVar('T1') T2 = TypeVar('T2')

def pair(first: T1, second: T2) -> tuple[T1, T2]: """Create a pair of potentially different types.""" return (first, second)

def map_pair(pair: tuple[T1, T2], func1: Callable[[T1], T1], func2: Callable[[T2], T2]) -> tuple[T1, T2]: """Apply functions to both elements of a pair.""" return (func1(pair[0]), func2(pair[1]))

=============================================================================

SECTION 3: Generic Classes

=============================================================================

class Stack(Generic[T]): """Generic stack data structure."""

def __init__(self) -> None:
    self._items: List[T] = []

def push(self, item: T) -> None:
    """Push an item onto the stack."""
    self._items.append(item)

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

def peek(self) -> Optional[T]:
    """Peek at top item without removing."""
    return self._items[-1] if self._items else None

def is_empty(self) -> bool:
    """Check if stack is empty."""
    return len(self._items) == 0

class Pair(Generic[T1, T2]): """Generic pair of two potentially different types."""

def __init__(self, first: T1, second: T2) -> None:
    self.first = first
    self.second = second

def get_first(self) -> T1:
    return self.first

def get_second(self) -> T2:
    return self.second

def swap(self) -> 'Pair[T2, T1]':
    """Return a new pair with swapped elements."""
    return Pair(self.second, self.first)

=============================================================================

SECTION 4: Constrained TypeVars

=============================================================================

from typing import Union

TypeVar with constraints

NumberType = TypeVar('NumberType', int, float)

def add(x: NumberType, y: NumberType) -> NumberType: """Add two numbers (int or float).""" return x + y # type: ignore

def multiply(x: NumberType, y: NumberType) -> NumberType: """Multiply two numbers.""" return x * y # type: ignore

=============================================================================

SECTION 5: Bounded TypeVars

=============================================================================

class Comparable: """Base class for comparable objects.""" def lt(self, other: 'Comparable') -> bool: raise NotImplementedError

ComparableType = TypeVar('ComparableType', bound=Comparable)

def find_min(items: Sequence[ComparableType]) -> Optional[ComparableType]: """Find minimum item (must be comparable).""" if not items: return None return min(items)

=============================================================================

SECTION 6: Practical Examples

=============================================================================

class Box(Generic[T]): """A container that holds a value."""

def __init__(self, value: T) -> None:
    self._value = value

def get(self) -> T:
    """Get the contained value."""
    return self._value

def set(self, value: T) -> None:
    """Set a new value."""
    self._value = value

def map(self, func: Callable[[T], T]) -> 'Box[T]':
    """Apply a function to the contained value."""
    return Box(func(self._value))

def filter_list(items: List[T], predicate: Callable[[T], bool]) -> List[T]: """Filter items using a predicate.""" return [item for item in items if predicate(item)]

if name == "main": print("=== Generic Types Examples ===\n")

# Basic TypeVar
print("TypeVar:")
print(f"  identity(5): {identity(5)}")
print(f"  identity('hello'): {identity('hello')}")
print(f"  get_first([1,2,3]): {get_first([1,2,3])}")
print()

# Generic classes
print("Generic Classes:")
int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(f"  Stack pop: {int_stack.pop()}")

str_stack: Stack[str] = Stack()
str_stack.push("hello")
print(f"  String stack peek: {str_stack.peek()}")
print()

# Pair
p = Pair(10, "hello")
print(f"  Pair: ({p.get_first()}, {p.get_second()})")
swapped = p.swap()
print(f"  Swapped: ({swapped.get_first()}, {swapped.get_second()})")

```


Exercises

Exercise 1. Write a generic function first(items: Sequence[T]) -> T using TypeVar that returns the first element of any sequence. Demonstrate it with both a list of ints and a tuple of strings.

Solution to Exercise 1

```python from typing import TypeVar, Sequence

T = TypeVar("T")

def first(items: Sequence[T]) -> T: return items[0]

print(first([1, 2, 3])) # 1 (int) print(first(("a", "b", "c"))) # a (str) ```


Exercise 2. Create a generic class Stack[T] with push(item: T), pop() -> T, and peek() -> T methods. Annotate everything properly.

Solution to Exercise 2

```python from typing import TypeVar, Generic

T = TypeVar("T")

class Stack(Generic[T]): def init(self) -> None: self._items: list[T] = []

def push(self, item: T) -> None:
    self._items.append(item)

def pop(self) -> T:
    return self._items.pop()

def peek(self) -> T:
    return self._items[-1]

s: Stack[int] = Stack() s.push(1) s.push(2) print(s.peek()) # 2 print(s.pop()) # 2 ```


Exercise 3. Explain the difference between TypeVar("T") and TypeVar("T", bound=Comparable). When would you use a bounded TypeVar?

Solution to Exercise 3

TypeVar("T") is unconstrained — it can be any type. TypeVar("T", bound=Comparable) restricts T to types that are subtypes of Comparable.

Use a bounded TypeVar when you need to call methods or use operations that only certain types support. For example, TypeVar("T", bound=SupportsLessThan) ensures T has a __lt__ method, which is needed for sorting.


Exercise 4. Write a function merge_sorted(a: list[T], b: list[T]) -> list[T] using TypeVar that merges two sorted lists into one sorted list. Test it with integers and strings.

Solution to Exercise 4

```python from typing import TypeVar

T = TypeVar("T")

def merge_sorted(a: list[T], b: list[T]) -> list[T]: result: list[T] = [] i = j = 0 while i < len(a) and j < len(b): if a[i] <= b[j]: result.append(a[i]) i += 1 else: result.append(b[j]) j += 1 result.extend(a[i:]) result.extend(b[j:]) return result

print(merge_sorted([1, 3, 5], [2, 4, 6])) # [1, 2, 3, 4, 5, 6] print(merge_sorted(["a", "c"], ["b", "d"])) # ['a', 'b', 'c', 'd'] ```


Exercise 5. Use TypeVar with constraints (TypeVar("T", int, float)) to write a function add(a: T, b: T) -> T that only works with numeric types. Show that passing strings would be flagged by mypy.

Solution to Exercise 5

```python from typing import TypeVar

T = TypeVar("T", int, float)

def add(a: T, b: T) -> T: return a + b

print(add(1, 2)) # 3 print(add(1.5, 2.5)) # 4.0

add("a", "b") # mypy error: Value of type variable "T" cannot be "str"

```