skip to content

What does typing.TypeVarTuple express that a single TypeVar cannot?

level: seniorimportance: nice to knowfreq 14%

answer

  1. One placeholder is not enough
  2. Variable number of types, each remembered
  3. Must be unpacked in a subscript
  4. Star syntax or the explicit form
  5. Only one per parameter list

basics

~20 s

A TypeVar stands for one type; a TypeVarTuple stands for an arbitrary-length sequence of types. It lets a signature carry a variable number of heterogeneous positional types through, such as tuple[int, *Ts] preserving each element's type.

solid answer

~40 s

`typing.TypeVar` binds exactly one type, so a function taking `*args: T` forces every argument to the same type. `typing.TypeVarTuple` (PEP 646, Python 3.11) is a **variadic** type variable: it binds to a whole sequence of types of unknown length, each remembered individually. You declare `Ts = TypeVarTuple("Ts")` and unpack it in subscripts, either as `tuple[int, *Ts]` with the star syntax or as `tuple[int, Unpack[Ts]]` with `typing.Unpack`. The typical uses are forwarding `*args` without collapsing the element types, returning a transformed tuple whose shape mirrors the input, and shape-parameterised containers in array-style libraries. One TypeVarTuple may appear per signature, and it must be the variable-length part; `Unpack` also serves a second role, unpacking a `TypedDict` into `**kwargs` since 3.12.

code

python · 8 lines
python
from typing import TypeVarTuple, Unpack

Ts = TypeVarTuple("Ts")

def tag(invoice_id: int, *rest: Unpack[Ts]) -> tuple[int, Unpack[Ts]]:
    return (invoice_id, *rest)

print(tag(83, "invoice.pdf", 2.5))

go deeper

for a junior

Nothing is expected here beyond knowing that generics can involve more than one type parameter. This corner of typing is not screening material at your level.

for a middle

Recognise the name and its one-line purpose: a type variable standing for a variable number of types rather than one, always written unpacked in a subscript. Reading it in a library's stubs is the realistic bar.

for a senior

Explain what it buys over a plain TypeVar and where the boundary with ParamSpec falls, and be honest that most codebases never declare one. Knowing the star and explicit unpack spellings and their version floor is a plus, not a gate.

for a principal

Judge whether the complexity pays: shape-typed containers can encode real invariants, but they raise the reading cost of every signature and lean on checker support. Decide where the team stops and accepts a looser annotation.

## One type versus a sequence of types A `TypeVar` is a placeholder for a single type. That is enough for `def first(items: list[T]) -> T`, and it is not enough for the moment a signature has to carry *several* types whose count is unknown: ```python def tag(invoice_id: int, *rest: T) -> tuple[int, ...]: ... ``` Annotating `*rest: T` forces every extra argument to the same type, and the return `tuple[int, ...]` has already lost which type each position held. Calling `tag(83, "invoice.pdf", 2.5)` either fails to check or degrades to `tuple[int, ...]` — the `str` and the `float` are gone. `typing.TypeVarTuple`, introduced by PEP 646 in Python 3.11, is a type variable that binds to an **arbitrary-length sequence of types**, remembering each element separately. It is usually called a *variadic* type variable, and the feature as a whole is *variadic generics*. ## Declaring and unpacking it ```python Ts = TypeVarTuple("Ts") def tag(invoice_id: int, *rest: Unpack[Ts]) -> tuple[int, Unpack[Ts]]: ... ``` Because a TypeVarTuple stands for many types rather than one, it never appears bare in a subscript — it must be **unpacked**, and there are two equivalent spellings: * the star form, `tuple[int, *Ts]`, which reads like iterable unpacking and is available in subscripts from 3.11; * the explicit form, `tuple[int, Unpack[Ts]]`, using `typing.Unpack`. They mean the same thing. Now `tag(83, "invoice.pdf", 2.5)` is understood as returning `tuple[int, str, float]`, with each position's type intact. From Python 3.12, the inline type-parameter syntax spells the declaration in the signature itself — `def tag[*Ts](...)` — with no separate `TypeVarTuple(...)` assignment. ## Where it actually earns its place Three families of use come up: * **Faithful `*args` forwarding for plain functions.** A helper that accepts any positional arguments and returns them repackaged — reversed, prefixed, zipped with a key — can keep every element's type instead of returning `tuple[Any, ...]`. Note the neighbouring tool: when the thing being forwarded is *another function's whole signature*, `ParamSpec` is the right variable, because it also carries keyword parameters and defaults. `TypeVarTuple` handles positional types only. * **Shape-typed containers.** The motivating use case in PEP 646 was array libraries wanting the dimensionality of an array in its type — a container parameterised by a variable number of axis types, so that an operation reducing one axis is visible to a checker. Libraries of that kind, and any homegrown container with the same shape, are the deep end of the feature. * **Tuple transformations.** Functions whose result is structurally derived from a variable-length input tuple, such as prepending a constant field to every record, express their contract exactly rather than as `tuple[Any, ...]`. ## Rules and limits Only **one** TypeVarTuple may appear in a single parameter list or class type-parameter list, for the same reason a function may have only one `*args`: two variable-length holes would make the split ambiguous. It can be combined with ordinary TypeVars around it — `def f[T, *Ts](first: T, *rest: *Ts)` — and the fixed parameters may sit before or after the variadic one in a tuple subscript, since a checker can still match positions from both ends when only one hole exists. There is no `bound=` for a TypeVarTuple: you cannot say "any number of types, all of which are numbers". PEP 696 added defaults for type parameters in 3.13, which includes variadic ones. ## The second job of Unpack `typing.Unpack` has a use unrelated to variadic tuples that is worth knowing because it shares the name. Since Python 3.12 (PEP 692), unpacking a `TypedDict` into `**kwargs` types keyword arguments precisely: ```python class RenderOptions(TypedDict): watermark: str dpi: int def render(**kwargs: Unpack[RenderOptions]) -> bytes: ... ``` Without it, `**kwargs: str` means "every keyword value is a `str`". With it, each keyword has its own declared type and its own required-or-not status. Same spelling, different mechanism — a stem or a review comment that says "unpack" without saying which is ambiguous. ## Honest positioning Variadic generics are the least-used corner of Python's generics. Most codebases never declare a `TypeVarTuple`; they meet one only when reading the stubs of an array library. Knowing that it exists, what problem it solves, and that `ParamSpec` — not `TypeVarTuple` — is the tool for forwarding a *signature* is the level of familiarity that matters in practice.

  • Why can only one TypeVarTuple appear in a signature?
    For the same reason a function may have only one `*args`: two variable-length holes make the split between them ambiguous, since a checker could not decide which types belong to which. With exactly one hole, fixed positions can still be matched from the left and the right, so ordinary TypeVars may sit on either side of it.
  • When would you reach for ParamSpec instead of TypeVarTuple?
    Whenever what you are forwarding is another function's whole signature. A ParamSpec captures positional and keyword parameters together with their names and defaults, which is what a decorator needs. A TypeVarTuple only captures a sequence of positional types, so it cannot describe a wrapper that must accept whatever the wrapped function accepts.
  • What does Unpack mean when it is applied to a TypedDict?
    That is the separate PEP 692 use, available from Python 3.12: `**kwargs: Unpack[SomeTypedDict]` gives each keyword argument its own declared type and required-or-not status, instead of the blanket "every value is this one type" that a plain annotation on `**kwargs` means. Same spelling as variadic unpacking, entirely different mechanism.

saying these in an interview costs you the question

  • Thinks *args: T can preserve mixed argument types
  • Uses a bare TypeVarTuple in a subscript without unpacking
  • Reaches for it to type a decorator instead of ParamSpec
  • Believes several TypeVarTuples may share one signature
  • Expects a bound= constraint on a TypeVarTuple
  • Confuses unpacking a TypedDict with variadic tuple unpacking

context