Skip to content

← Interfaces with Protocols and ABCs step 23 of 26

Hard Primitives

Keep the Type at Each Tuple Position with TypeVarTuple

This exercise is optional. It covers TypeVarTuple, a feature that most code does not need, so you can skip it on a first pass.

Some helpers build tuples of different lengths. pairs("ab", 3) returns ("ab", 3), and prepend(True, ("ab", 3)) returns (True, "ab", 3). The starter types their results with object, as in tuple[object, ...], a tuple of any length whose items are only known to be objects. So mypy loses the type at each position. Submit the starter, and mypy rejects three[1].upper():

L18:14 [attr-defined]
"object" has no attribute "upper"

The code runs, because three[1] is a string at runtime. The tests pass, and mypy is the check that fails.

A type parameter for a sequence of types

An ordinary type parameter such as T stands for one type. A TypeVarTuple, written *Ts, stands for a sequence of types, in order:

def pairs[*Ts](*args: *Ts) -> tuple[*Ts]:
    return args

If label is a str and count is an int, pairs(label, count) has the type tuple[str, int]. With no arguments, Ts is empty and the result is tuple[()]. You can put ordinary type parameters before or after it, so tuple[T, *Ts] is a T followed by the types in Ts. One tuple type can hold only one *Ts.

What to write

Give the three helpers these signatures, and keep solve as it is:

  • pairs[*Ts](*args: *Ts) -> tuple[*Ts] returns its arguments as a tuple, in order. With no arguments it returns ().
  • prepend[T, *Ts](head: T, rest: tuple[*Ts]) -> tuple[T, *Ts] returns head followed by the items of rest. rest may be empty.
  • ends[B, *Rest, C](shape: tuple[B, *Rest, C]) -> tuple[B, C] returns the first and last items. Its input has at least two items, and the middle part may be empty.

Return tuples, not lists, and do not change the arguments.

What the driver returns

solve(label, count, flag) builds these values. The last column is the type mypy infers once your signatures are in place:

Key Value Type
two (label, count) tuple[str, int]
three (flag, label, count) tuple[bool, str, int]
four (count, flag, label, count) tuple[int, bool, str, int]
tagged label.upper() str
ends (count, count) tuple[int, int]

tagged uses Python's own upper(), so "ß" becomes "SS".

Going further

TypeVarTuple arrived in Python 3.11 with PEP 646, and the [*Ts] syntax in Python 3.12. The main example in PEP 646 is the shape of an array, such as Array[Height, Width]. A TypeVarTuple describes positional types only. For keyword arguments, use Unpack with a TypedDict, as earlier in this track.