typing.TypeVarTuple represents a variable number of type parameters. A normal TypeVar captures one type, while TypeVarTuple can capture a heterogeneous sequence such as (int, str, bytes) and reuse it elsewhere. This enables variadic generics, position-preserving tuple transformations, and shape-aware array models.
This guide covers declarations, Unpack, starred syntax, prefixes and suffixes, generic classes, dimensions, inference limits, callable comparisons, runtime behavior, introspection, compatibility, and testing.
The limit of a normal TypeVar
from typing import TypeVar
T = TypeVar("T")
def repeat(value: T) -> tuple[T, T]:
return (value, value)TypeVar preserves one type. It cannot capture an unknown number of distinct tuple positions.
Homogeneous tuples
def count(values: tuple[int, ...]) -> int:
return len(values)tuple[int, ...] allows any number of integers. Every position shares the same type. It does not preserve tuple[int, str, bool] as three separate types.
Declaring TypeVarTuple
from typing import TypeVarTuple
Ts = TypeVarTuple("Ts")Ts represents zero or more positional types. It must be expanded when used.
Expanding with Unpack
from typing import Unpack
def tuple_identity(
values: tuple[Unpack[Ts]],
) -> tuple[Unpack[Ts]]:
return valuesAn input of tuple[int, str] produces the same output type. Every position is retained.
Starred syntax
def tuple_identity(values: tuple[*Ts]) -> tuple[*Ts]:
return valuesIn modern contexts, *Ts is equivalent to Unpack[Ts]. The explicit form remains useful for compatibility.
Adding a prefix
def with_name(
values: tuple[*Ts],
) -> tuple[str, *Ts]:
return ("record", *values)An input tuple[int, bool] becomes tuple[str, int, bool].
Adding a suffix
def with_status(
values: tuple[*Ts],
) -> tuple[*Ts, bool]:
return (*values, True)Fixed prefixes and suffixes model uniform transformations of positional records.
Removing the first position
def tail(
values: tuple[object, *Ts],
) -> tuple[*Ts]:
first, *rest = values
return tuple(rest)The annotation requires at least one element. Some checkers may need help relating the intermediate list to the final variadic tuple.
Variadic generic classes
from typing import Generic
class Record(Generic[*Ts]):
def __init__(self, values: tuple[*Ts]) -> None:
self.values = valuesSpecializations can have different numbers of parameters:
row: Record[int, str]
pixel: Record[int, int, int, float]The class preserves each instance’s positional structure.
Typed zip relationships
A fully generic zip function must relate several iterables to an output tuple. TypeVarTuple can express parts of the relationship, but individual iterable inference may still require overloads or more advanced support.
Modeling shapes
Shape = TypeVarTuple("Shape")
class Array(Generic[*Shape]):
...A matrix can be Array[Height, Width] and an image Array[Height, Width, Channels]. These are type markers, not runtime numeric sizes.
Adding a batch dimension
class Batch: ...
def add_batch(x: Array[*Shape]) -> Array[Batch, *Shape]:
...The operation preserves all existing dimensions and adds one at the front.
Transposition limits
TypeVarTuple preserves a sequence but does not provide general type-level reversal or permutation. For two-dimensional matrices, explicit type parameters or overloads may be clearer.
One TypeVarTuple per parameter list
# Ambiguous: tuple[*As, *Bs]A checker cannot determine where one variadic group ends and the next begins. Use one group with fixed elements around it.
The group may be empty
A TypeVarTuple can capture zero types. APIs that require at least one position should include a fixed prefix or suffix in the annotation.
Constraints
TypeVarTuple does not provide exactly the same per-element bounds and constraints as TypeVar. When every element must share one interface, a homogeneous tuple or another abstraction may be more appropriate.
TypeVarTuple and Unpack
TypeVarTuple defines the group; Unpack expands it into positions or generic parameters. The Python Unpack guide also covers typed kwargs.
Inference from tuple literals
result = tuple_identity((1, "a", True))A checker can infer tuple[int, str, bool]. Use assert_type() to protect that expectation in static tests.
Lists do not preserve positions
A list normally has one element type such as list[int | str]. It does not retain a different type for each index. TypeVarTuple naturally fits tuples and generic parameter lists rather than mutable heterogeneous lists.
Callable parameters
TypeVarTuple is not a replacement for ParamSpec. ParamSpec preserves callable parameters, including names, keyword-only status, and kwargs. TypeVarTuple preserves a positional sequence of types.
Comparison with overloads
Without TypeVarTuple, a library might write overloads for tuples of length one, two, three, and four. A variadic group removes repetition when the transformation is uniform for any length.
Runtime behavior
TypeVarTuple does not validate lengths or values at runtime. A class must still enforce invariants normally. Generic arguments may be erased or only partially available through introspection.
Introspection
get_origin() and get_args() help inspect specializations, but instances do not always retain all generic information. Do not base security or data integrity on typing metadata alone.
Compatibility
Use typing_extensions.TypeVarTuple and Unpack for older supported versions. Checker support also matters, so test with current mypy or pyright versions.
Common mistakes
- Using TypeVarTuple without expansion: write
*TsorUnpack[Ts]. - Confusing it with tuple[T, …]: it preserves distinct positional types.
- Creating two variadic groups: argument division becomes ambiguous.
- Expecting runtime shape validation: the relationship is static.
- Replacing ParamSpec with it: callable signatures need more than positional types.
- Modeling beyond checker support: simplify the API when necessary.
Complete example: record pipeline
from typing import Generic, TypeVarTuple
Fields = TypeVarTuple("Fields")
class Row(Generic[*Fields]):
def __init__(self, data: tuple[*Fields]) -> None:
self.data = data
def number(row: Row[*Fields]) -> Row[int, *Fields]:
return Row((1, *row.data))
def mark(row: Row[*Fields]) -> Row[*Fields, bool]:
return Row((*row.data, True))
source = Row(("Ana", 42.0))
numbered = number(source)
marked = mark(numbered)The type evolves from Row[str, float] to Row[int, str, float] and then Row[int, str, float, bool]. Each transformation preserves prior positions.
Static tests
from typing import assert_type
assert_type(numbered.data, tuple[int, str, float])
assert_type(marked.data, tuple[int, str, float, bool])These tests protect inference during implementation and annotation changes.
When to avoid TypeVarTuple
Use a dataclass or NamedTuple when fields have stable names. Use a homogeneous tuple when every element shares one type. Use ParamSpec for function wrappers. Use TypeVarTuple when the number of positions varies and each positional type must be preserved.
Conclusion
TypeVarTuple extends Python generics to variable sequences of types. It preserves heterogeneous tuples, supports classes with varying type parameters, and models shape transformations without dozens of overloads.
The official Python TypeVarTuple documentation defines the rules. Expand it with Unpack or starred syntax, keep one variadic group per parameter list, and verify behavior with static tests across supported checkers.







