Back to Blog
Python

Python Type Parameter Syntax

python type parameter syntax: Learn the modern Python type parameter syntax introduced in PEP 695, how it compares to the older TypeVar approach, and when to use each...

PEP 695GenericsType HintsTypeVarStatic Typing
Illustration of Python generic type parameter syntax showing a boxed T type parameter between square brackets in a code editor.

The python type parameter syntax changed significantly with the release of Python 3.12. The traditional approach using typing.TypeVar is still valid, but a new, more concise syntax now exists for declaring generic functions, classes, and type aliases. This article explains the new syntax, shows how it maps to the old approach, and covers practical considerations for adopting it in your codebase.

The New Syntax: PEP 695

PEP 695 introduces a dedicated syntax for type parameters. Instead of creating a TypeVar and then using it in a function signature, you can now declare type parameters directly in the function or class definition. The syntax uses square brackets after the function name or class name.

For a generic function, the type parameter is declared between the function name and the parameter list:

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

For a generic class, the type parameter is declared after the class name:

class Stack[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()

This syntax is more direct and easier to read. The type parameter T is in scope for the entire function or class body, including method signatures and attribute annotations.

Comparing the Old TypeVar Approach

Before Python 3.12, you had to create a TypeVar explicitly and then reference it. The equivalent generic function using typing.TypeVar looks like this:

from typing import TypeVar T = TypeVar("T") def first_element(items: list[T]) -> T: return items[0]

The class version is similar:

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()

The old approach requires more boilerplate. You must import TypeVar, create an instance, and for classes, inherit from Generic[T]. The new syntax eliminates this extra code and keeps the type parameter definition local to where it is used.

Type Parameter Bounds and Constraints

The new syntax also supports bounds and constraints, which restrict what types can be used as arguments for the type parameter.

A bound restricts the type parameter to a specific type or its subclasses. In the new syntax, you use the : operator:

def max_value[T: int | float](items: list[T]) -> T: return max(items)

This is equivalent to the old TypeVar with a bound:

from typing import TypeVar T = TypeVar("T", bound=int | float) def max_value(items: list[T]) -> T: return max(items)

Constraints, which limit the type parameter to an explicit set of types, use the : operator with a union type in the new syntax. The old syntax used a tuple of types in TypeVar:

# New syntax def parse_int_or_float[T: int | float](value: str) -> T: ... # Old syntax from typing import TypeVar T = TypeVar("T", int, float) def parse_int_or_float(value: str) -> T: ...

Note that the new syntax treats T: int | float as a constraint set, not a bound. The type parameter can only be exactly int or float, not a subclass like bool (which is a subclass of int). This matches the behavior of the old tuple-based constraints.

Type Aliases with Type Parameters

The new syntax also simplifies generic type aliases. In the old approach, you would write:

from typing import TypeAlias, TypeVar T = TypeVar("T") Result: TypeAlias = tuple[T, str]

With PEP 695, you can declare a generic type alias directly:

type Result[T] = tuple[T, str]

This is cleaner and makes the type parameter explicit in the alias declaration. The alias can then be used like any other generic type:

def process(value: int) -> Result[int]: return (value, "ok")

Scope and Reusability Differences

One important difference between the old and new syntax is the scope of the type parameter. In the old approach, a TypeVar is a module-level object. You can reuse the same TypeVar in multiple functions or classes within the same module. This can be useful when you want to enforce a relationship between separate functions.

For example, you might define a TypeVar and use it in two different functions to indicate that they operate on the same type:

from typing import TypeVar T = TypeVar("T") def get_item() -> T: ... def set_item(item: T) -> None: ...

With the new syntax, each function or class has its own type parameter, even if they share the same name. This is usually what you want, as it avoids accidental coupling. However, if you need to share a type parameter across multiple functions, the old TypeVar approach is still the way to do it.

Runtime Behavior and Compatibility

PEP 695 type parameters are evaluated at runtime. The __type_params__ attribute on the function or class object holds the type parameter objects. This is similar to how __parameters__ works for generic classes in the old system, but the new syntax provides a more direct introspection path.

For example:

def first_element[T](items: list[T]) -> T: return items[0] print(first_element.__type_params__)

This will print a tuple containing the TypeVar object for T. This is useful for libraries that need to introspect generic functions or classes.

A key compatibility consideration is that the new syntax requires Python 3.12 or later. If you are writing code that must run on earlier Python versions, you cannot use PEP 695 syntax. In that case, you must stick with the typing.TypeVar approach. Type checkers like mypy and pyright support the new syntax, but only when the target Python version is set to 3.12 or higher.

When to Use Which Syntax

For new code that targets Python 3.12 or later, the PEP 695 syntax is the preferred choice. It reduces boilerplate, improves readability, and keeps type parameters local to their use. The old TypeVar syntax remains necessary when you need to share a type parameter across multiple functions or classes, or when you must maintain compatibility with Python versions before 3.12.

There is no performance difference between the two approaches at runtime. The type parameter objects are created at definition time, and both approaches result in similar introspection capabilities. The choice is primarily about code clarity and version support.

When migrating existing code, you can incrementally adopt the new syntax. A function that uses a module-level TypeVar only once can be safely converted. If a TypeVar is used in several places, you must decide whether to keep it shared or convert each use to its own type parameter, depending on whether the relationship between those uses is intentional.

The new type statement for aliases is a clear improvement over the old TypeAlias assignment, but it also requires Python 3.12. For libraries that support multiple Python versions, the old syntax will remain in use for the foreseeable future. Understanding both forms is essential for reading and writing modern Python code.

python type parameter syntax: Practical Usage and Code Examp | RYUSLOG DEV