Python type parameter syntax lets you declare generic types directly beside a function or class name. Introduced in Python 3.12, it replaces much of the repeated TypeVar and Generic setup used by older typed code.
You use this syntax when a function, class, or type alias must preserve a relationship between input and output types. Static type checkers and editors can then detect mistakes while the implementation remains reusable.
Why Type Parameters Matter
A type such as object accepts many values, but it loses the connection between the value you pass and the value you receive. A type parameter gives that unknown type a local name, commonly T, and reuses it throughout the declaration.
| Declaration | Purpose | Minimum Python |
|---|---|---|
| def name[T](...) | Generic function | 3.12 |
| class Name[T]: | Generic class | 3.12 |
| type Name[T] = ... | Generic type alias | 3.12 |
| T = default | Default type argument | 3.13 |
Create a Generic Function
Place the type parameter in square brackets after the function name. The following function accepts a list containing one type and returns a value of that same type.
Example:
# Python 3.12+: declare T next to the function name.
def first[T](items: list[T]) -> T:
if not items:
raise ValueError("items cannot be empty")
return items[0]
# The return type follows the element type of each list.
print(first([12, 18, 25]))
print(first(["Aisha", "Oliver", "Maya"]))
Output:
12
Aisha
A type checker understands that the first call returns an integer and the second returns a string. Python still executes the annotations as type information; it does not automatically reject a wrong value at runtime.
Create a Generic Class
A class can declare a type parameter after its name. Use the parameter for attributes, constructor arguments, and return types that must agree.
Example:
class Box[T]:
def __init__(self, value: T) -> None:
# Store exactly the type supplied to this Box.
self.value = value
def get(self) -> T:
return self.value
order_id = Box[int](1042)
customer = Box[str]("Noah")
print(order_id.get())
print(customer.get())
Output:
1042
Noah
You can often omit the explicit type argument because a type checker can infer it from the constructor value, such as Box("Noah").
Define a Generic Type Alias
The type statement creates a named alias. Add parameters when the alias should work with several data types.
# A result is either a value of T or an error message.
type Result[T] = tuple[T | None, str | None]
def divide(a: float, b: float) -> Result[float]:
if b == 0:
return None, "Division by zero"
return a / b, None
The type word is a soft keyword. Existing variables or methods named type usually continue to work because Python treats it specially only in the alias statement's context.
Restrict a Type Parameter with a Bound
A bound says that the type argument must be the bound itself or a compatible subtype. This lets the implementation safely use operations supplied by that bound.
Example:
from collections.abc import Sized
# T must provide __len__ through the Sized protocol.
def longer[T: Sized](left: T, right: T) -> T:
return left if len(left) >= len(right) else right
print(longer("Delhi", "London"))
print(longer([1, 2], [4, 5, 6]))
Output:
London
[4, 5, 6]
Use Constraints for a Fixed Set of Types
Constraints allow one of several listed types. Write the choices as a tuple after a colon.
# Accept text or bytes, but do not mix their return relationship.
def duplicate[T: (str, bytes)](value: T) -> T:
return value + value
print(duplicate("Hi "))
print(duplicate(b"OK"))
Output:
Hi Hi
b'OKOK'
Declare Variadic Type Parameters
Python also supports a type variable tuple with *Ts and a parameter specification with **P. These forms describe an arbitrary sequence of types or preserve a callable's complete parameter list.
from collections.abc import Callable
# Preserve all positional and keyword parameters of the wrapped callable.
def traced[**P, R](func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("Calling", func.__name__) # Simple trace message
return func(*args, **kwargs)
return wrapper
Compare New and Legacy Syntax
| Python 3.12+ syntax | Legacy approach |
|---|---|
| Parameter declared beside the name | TypeVar declared at module level |
| No Generic base required for a basic class | Class commonly inherits from Generic |
| Scope is limited to the declaration | Type variable exists as a normal module symbol |
| Variance can be inferred by type checkers | Variance may require explicit TypeVar options |
Compatibility and Best Practices
- Use the new syntax only when your runtime and tooling support Python 3.12 or later.
- Keep
TypeVar-based syntax in libraries that still support Python 3.11 and earlier. - Choose short names such as
T,K, andVwhen their meaning is clear; use descriptive names for complex APIs. - Run a static type checker because Python annotations do not enforce values automatically.
- Use a bound for an open family of compatible types and constraints for a small fixed set.
Tip: Python 3.13 added default type parameter values. Confirm your project's minimum version before using that newer syntax.
Conclusion
Python type parameter syntax makes generic functions, classes, and aliases shorter and easier to read. Start with a simple [T], add bounds or constraints only when required, and retain legacy declarations when older Python versions remain part of your support policy.