Skip to content

← Structural Typing and the Hard Parts step 12 of 24

Hard Primitives

TypeVarTuple: generics over an arbitrary number of types

A TypeVar abstracts over one type. A TypeVarTuple abstracts over an arbitrary number of them. It is what makes tuple[str, int] expressible as the return of a function whose arity is not known in advance — and, in the world this site’s users live in, it is what makes shape-typed array signatures like Array[Batch, *Shape, Channels] possible at all.

The rules

  • At most one TypeVarTuple per parameter list.
  • It is always written unpacked: *Ts, never a bare Ts.
  • It combines with fixed types on either side: tuple[Batch, *Shape, Channels].
  • *args: *Ts gives args the type tuple[*Ts] — the exact element types and the exact arity, not tuple[object, ...].
  • An unparameterised variadic class defaults to *tuple[Any, ...].

What PEP 646 explicitly does not do

Two non-goals, stated in the PEP, that save days if you know them up front:

  • No shape arithmetic. You cannot express “the output has dimension n + 1“ or “these two dimensions multiply”. Type parameters are not integers.
  • No `kwargs` pairing.** There is no variadic analogue for keyword arguments.

If you need either, the answer is a runtime check, not a cleverer annotation.

Your task

def pairs[*Ts](*args: *Ts) -> tuple[*Ts]
def prepend[T, *Ts](head: T, rest: tuple[*Ts]) -> tuple[T, *Ts]
def ends[B, *Rest, C](shape: tuple[B, *Rest, C]) -> tuple[B, C]
def solve(label: str, count: int, flag: bool) -> dict[str, object]

solve builds two = pairs(label, count), three = prepend(flag, two), four = prepend(count, three), takes tagged = three[1].upper() and first, last = ends(four), and returns {"two", "three", "four", "tagged", "ends"}.

Where the type system does the work

three[1].upper() is the assertion. three is tuple[bool, str, int], so element 1 is a str and .upper() resolves. Annotate pairs as (*args: object) -> tuple[object, ...] — the “reasonable” non-variadic version — and that line stops compiling, because the arity and the per-position types have been thrown away.

ends is the other half: it shows a TypeVarTuple sandwiched between two ordinary type parameters, with shape[0] and shape[-1] resolving to B and C respectively.

Return shapes matter

The tests compare tuples as tuples. Returning a list where a tuple is expected fails, deliberately — in Python the container type is frequently the thing under test.