Variadic Generics with TypeVarTuple (PEP 646)

A TypeVarTuple is a single type variable that stands for an arbitrary number of types at once. Where an ordinary TypeVar captures one type, a TypeVarTuple captures a whole sequence — enabling generic classes and functions whose arity is not fixed, such as a tensor typed by its shape or a zip-like function typed by each of its inputs.

Introduced by PEP 646 and shipped in Python 3.11, variadic generics fill a long-standing gap: before them, there was no way to write a Array[Height, Width] whose number of dimensions varied, nor to preserve every element type of a heterogeneous tuple through a transformation. This cluster covers the syntax, the Unpack/*Ts mechanics, and the current state of analyzer support.

How a TypeVarTuple expands A single TypeVarTuple named Ts binds to a sequence of concrete types, which then unpack in position inside a tuple annotation, optionally with a fixed prefix element. TypeVarTuple Ts = *Ts binds to a sequence (int, str, bytes) tuple[int, *Ts] tuple[int, int, str, bytes] fixed prefix + unpacked Ts
A single TypeVarTuple binds to a sequence of types, which unpack in place inside a tuple annotation.

The core syntax

You declare a TypeVarTuple and splice it into a tuple with the unpack star. On Python 3.11 the classic spelling uses TypeVarTuple from typing; the unpacked position can be written either as *Ts (the star syntax) or Unpack[Ts] (the explicit form). Both mean the same thing.

# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import TypeVarTuple, Unpack

Ts = TypeVarTuple("Ts")

def move_first_to_last(t: tuple[int, *Ts]) -> tuple[*Ts, int]:
    first, *rest = t
    return (*rest, first)

reveal_type(move_first_to_last((1, "a", b"z")))  # tuple[str, bytes, int]

The *Ts inside tuple[int, *Ts] says “an int, then however many further elements Ts stands for.” The return annotation reorders them, and the checker tracks every element type through the call.

PEP 695 form on 3.12

Python 3.12’s PEP 695 type parameter syntax lets you declare the variadic parameter inline with [*Ts], dropping the module-level TypeVarTuple object entirely.

# Python 3.12+, checked with mypy 1.10 / pyright 1.1.370
def stack[*Ts](*rows: *Ts) -> tuple[*Ts]:
    return rows

reveal_type(stack(1, "a", 3.0))  # tuple[int, str, float]

Here [*Ts] scopes the variadic to the function, and *rows: *Ts types the *args so that each positional argument keeps its own type in the returned tuple. This is the recommended spelling on 3.12+; the TypeVarTuple(...) object form remains for 3.11 and typing_extensions backports.

Shape-typed classes: the motivating case

The headline use for variadic generics is typing array and tensor shapes. A class Array[*Shape] is generic over an arbitrary number of dimension types, so a 2-D array and a 3-D array are distinct static types and a mismatched operation is a type error rather than a silent runtime bug.

# Python 3.12+, checked with pyright 1.1.370
from typing import NewType

Height = NewType("Height", int)
Width = NewType("Width", int)
Channels = NewType("Channels", int)

class Array[*Shape]:
    def __init__(self, *shape: *Shape) -> None:
        self._shape = shape

    @property
    def shape(self) -> tuple[*Shape]:
        return self._shape

image: Array[Height, Width, Channels] = Array(Height(1080), Width(1920), Channels(3))
reveal_type(image.shape)  # tuple[Height, Width, Channels]

The number of dimensions is encoded in the type itself, so a function that expects Array[Height, Width] rejects a three-dimensional Array before any numeric code runs.

Runtime vs static analysis A TypeVarTuple carries no runtime shape checking. Array(Height(1080), Width(1920)) stores an ordinary tuple; Python never verifies that the values match *Shape. If you pass three values where the annotation says two, the program runs fine and only a static checker flags it. Variadic generics document and enforce shape *at analysis time*, not at execution.

Analyzer support today

Support is uneven, so pin versions and test both checkers if you rely on variadics. Pyright has had strong, complete PEP 646 support for years, including prefix/suffix unpacking and [*Ts] scoping. mypy shipped TypeVarTuple support later and has steadily improved through the 1.x line; older releases mishandled a fixed suffix such as tuple[*Ts, int] or reported spurious [misc] errors on *args: *Ts. On current mypy (1.10+) the common patterns work, but edge cases around bounded variadics and nested unpacks can still diverge from pyright. See pyright vs mypy for how their solvers differ.

# Python 3.11+, checked with mypy 1.10 / pyright 1.1.370
from typing import TypeVarTuple, Generic

Ts = TypeVarTuple("Ts")

class Batch(Generic[*Ts]):  # a single *Ts is allowed; a second one is not
    ...

# class Bad(Generic[*Ts, *Us]): ...  # error: pyright reportGeneralTypeIssues

Exactly one TypeVarTuple may appear per generic — you can combine it with ordinary TypeVars, but not with a second variadic, because the checker could not decide where one sequence ends and the next begins.

Common mistakes

  • Two TypeVarTuples in one class or signature: Generic[*Ts, *Us] is rejected — mypy emits [misc] and pyright reportGeneralTypeIssues. Use one variadic plus fixed prefix/suffix elements instead.
  • Forgetting the star: writing tuple[int, Ts] instead of tuple[int, *Ts] treats Ts as a bare name; mypy reports [valid-type] / pyright reportInvalidTypeForm because an unpacked TypeVarTuple is required in that position.
  • Relying on 3.11 mypy for suffixes: tuple[*Ts, int] and tuple[int, *Ts, str] were buggy in early mypy releases. If a fixed suffix mis-resolves, upgrade mypy before assuming your annotation is wrong.
  • Expecting runtime enforcement: shape mismatches never raise on their own; add explicit assert len(shape) == 2 guards if you need runtime safety.

FAQ

When do I need a TypeVarTuple instead of a plain TypeVar? Reach for a TypeVarTuple only when the number of type parameters varies — heterogeneous tuples, zip/apply-style helpers, and shape-typed arrays. If every instance has the same fixed arity, ordinary TypeVars or a ParamSpec (for callable parameter lists) are simpler and better supported.

*How does Ts differ from ParamSpec’s P? A TypeVarTuple captures a sequence of positional type arguments that live inside a tuple; a ParamSpec captures an entire call signature, including keyword parameters, for forwarding to a Callable. They solve different problems and can even be combined in advanced adapter code.

Can I bound a TypeVarTuple to a single element type? Not directly — a TypeVarTuple has no bound=. If you need every element to share a type, a homogeneous tuple[T, ...] with a normal TypeVar expresses that better than a variadic.

Back to Advanced Typing Patterns & Generics