A generic preserves relationships between types without duplicating implementations. When a function accepts a value and returns the same type, TypeVar communicates more than Any.

Generic functions and classes

from typing import Generic, TypeVar

T = TypeVar("T")


def first(items: list[T]) -> T:
    if not items:
        raise ValueError("items cannot be empty")
    return items[0]


class Box(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value = value

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

A checker infers str for both first(["a"]) and Box("a").get(). Projects requiring Python 3.12 or newer can use the shorter def first[T](...) syntax; match the project's declared compatibility.

Bounds and constraints

A bound accepts subtypes of an upper limit, while constraints select from exact alternatives. Do not add variance manually without understanding whether the type is read, written, or both. Start invariant and introduce complexity for a concrete requirement.

The relationship a TypeVar preserves

Compare these signatures:

from typing import Any, TypeVar

T = TypeVar("T")


def weak_identity(value: Any) -> Any:
    return value


def identity(value: T) -> T:
    return value

With Any, a checker permits almost any operation on the result. With T, it connects the input and output types. identity("python") is a str, while identity(42) is an int. This creates no runtime type and converts no value; it gives the analyzer a variable to solve at each call.

A type variable used only once usually adds no information. def log(value: T) -> None can generally accept object, because there is no second position to relate. Reach for TypeVar when a type reappears in the return value, another parameter, or class state.

Multiple parameters and collections

Generics can connect input, callable, and result:

from collections.abc import Callable, Iterable

Input = TypeVar("Input")
Output = TypeVar("Output")


def transform(
    items: Iterable[Input],
    function: Callable[[Input], Output],
) -> list[Output]:
    return [function(item) for item in items]

For transform(["1", "20"], int), a checker infers list[int]. The function accepts any iterable rather than only a list because iteration is its sole requirement. Choosing the narrowest input protocol improves reuse without sacrificing precision.

Bounds for shared capabilities

An upper bound permits any subtype satisfying the stated base. Consider serializable objects:

from typing import Protocol


class Serializable(Protocol):
    def to_dict(self) -> dict[str, object]:
        ...


S = TypeVar("S", bound=Serializable)


def keep_original(item: S) -> S:
    item.to_dict()
    return item

The body may call to_dict, and the result retains the concrete type of item. Returning only Serializable would discard that information. A bound states a minimum capability, not a conversion.

Constraints differ: Text = TypeVar("Text", str, bytes) accepts those families and resolves the result to one listed alternative. A specific str subclass is promoted to str. Use constraints when the implementation truly supports a closed set and handles each option coherently. Prefer a bound or Protocol for an open capability.

Generic classes and encapsulation

A generic class carries its parameter across operations:

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:
        if not self._items:
            raise IndexError("empty stack")
        return self._items.pop()

Stack[str] accepts strings and returns str. Parameterization improves completion and catches inconsistent usage before execution. At runtime, however, it does not prevent stack.push(3). Validate untrusted JSON, form, or network data explicitly at the boundary.

Variance without guesswork

A container that both reads and writes T is normally invariant. Although Dog is a subtype of Animal, treating Stack[Dog] as Stack[Animal] would be unsafe because a caller could push a Cat. A read-only interface can be covariant; an input-only consumer can be contravariant.

Do not mark variance merely to silence an error. Inspect public operations and determine whether the parameter appears in input positions, output positions, or both. Modern type-parameter syntax can let checkers infer class variance; verify support in the project's Python and checker versions.

Python 3.12 syntax

PEP 695 introduced bracketed type parameters:

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


class Box[T]:
    def __init__(self, value: T) -> None:
        self.value = value

This form removes separate global declarations, but older interpreters cannot even parse the file. Libraries must honor their minimum supported version. Avoid mixing styles casually, and check the configured mypy, Pyright, or other analyzer version.

Pitfalls and practical criteria

Avoid returning Any from an otherwise generic function because it breaks the relationship you meant to preserve. Also avoid promising T without receiving or safely constructing it; a checker cannot know which concrete value to create. Casts can conceal design errors and should be rare and explained.

Start with concrete calling examples. If two positions must retain the same type, or a result depends on a supplied callable, a generic likely helps. If a function accepts heterogeneous values and returns unrelated information, object, a union, or a protocol may be more honest.

Inference, explicit types, and debugging

Let the checker infer parameters for most calls. Write box: Box[str] = Box("text") when the annotation documents a boundary, resolves an initially empty collection, or prevents an overly broad inference. Repeating obvious types on every local variable only adds noise.

When a checker produces a surprising result, reduce the example and inspect its inferred type with a feature such as reveal_type. Check every occurrence of T: parameters may impose competing requirements and make the analyzer choose a common ancestor. A function accepting two values of the same T does not guarantee identical runtime classes because a valid shared type may satisfy the call.

Generic aliases keep long structures readable. Python 3.12 supports type Result[T] = tuple[T, str | None]. On earlier versions, use compatible typing features. An alias names a data shape; it creates neither runtime validation nor a distinct nominal type.

Run the checker in CI with versioned configuration. Tighten rules gradually instead of covering errors with Any. Generic typing pays off when contracts are checked continuously and remain understandable to API maintainers.

Review Python type hints and mypy for analysis. The official typing documentation, accessed July 22, 2026, covers TypeVar, generics, and variance. Use generics for meaningful relationships, not to over-abstract a simple API.