Python TypeVarTuple: Variadic Generics

Published on: August 29, 2026
Reading time: 4 minutes
A close-up shot showcasing the intricate scales of a snake, highlighting texture and color.

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 values

An 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 values

In 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 = values

Specializations 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 *Ts or Unpack[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.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    A developer typing code on a laptop with a Python book beside in an office.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    assert_type and reveal_type: Test Type Inference

    Learn Python assert_type and reveal_type to inspect inference, test typed APIs, and prevent static typing regressions.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python get_type_hints: Resolve Annotations

    Learn Python get_type_hints to resolve forward references, preserve Annotated metadata, and inspect functions and classes safely.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python runtime_checkable: Protocol at Runtime

    Learn Python runtime_checkable to test Protocols with isinstance, understand its limits, and design safer structural contracts.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python Unpack: Typed kwargs and Variadics

    Learn Python Unpack to type **kwargs with TypedDict, expand variadic tuples, and preserve precise callable signatures.

    Ler mais

    Tempo de leitura: 4 minutos
    29/08/2026
    A person typing on a laptop with a Python programming book visible, capturing technology and learning.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Required and NotRequired: Optional TypedDict Keys

    Learn Python Required and NotRequired to control mandatory and optional TypedDict keys without confusing absence with None.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026
    High-angle view of woman coding on a laptop, with a Python book nearby. Ideal for programming and tech content.
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python ReadOnly: Protect TypedDict Fields

    Learn Python ReadOnly to protect TypedDict fields, model stable response contracts, and prevent accidental writes during static analysis.

    Ler mais

    Tempo de leitura: 5 minutos
    29/08/2026