Typing of nonhomogeneous tuples of undefined length


There’s been discussion on the typing of homogeneous tuples of fixed and variable length (e.g. here or here or PEP-484), for example

tuple[int, str]  # Two-tuple of an integer and string.
tuple[int, ...]  # Tuple of integers of variable length.
tuple  # Synonymous with tuple[Any, ...]

In the following I’d like to discuss nonhomogeneous tuples of variable length, for which I would like to express some type constraints.

For context: the question arose while I played around with SQLAlchemy’s declarative table configuration (docs) which states:

Keyword arguments can be specified with the above form [a tuple] by specifying the last argument as a dictionary

meaning that we have a tuple of some items, the last of which is a dictionary. This can’t currently be expressed, AFAIK.

So I started wondering if we could take an approach that’s somewhat similar to Python’s iterable unpacking of expression lists (docs), and interpret the ellipsis ... similarly to the *. For example

tuple[..., dict]

where the initial ellipsis ... would mean “zero or more items of type Any” with the last item being a dict. If we stick with that interpretation of the ellipsis then we’d also be able to express more elaborate shapes like

# Must contain an integer, then string, then anything, ending in a list.
tuple[int, str, ..., list]

Whould this make sense? Would this be something useful to add?


What you’re describing can be expressed thanks to the functionality introduced in PEP 646.

# Must contain an integer, then string, then anything, ending in a list.
v1: tuple[int, str, *tuple[Any, ...], list[Any]]
v1 = (1, "", []) # OK
v1 = (1, "", 1, "", []) # OK
v1 = (1, []) # Type error

# Must contain an integer, then zero or more strings, then a float.
v2: tuple[int, *tuple[str, ...], float]
v2 = (0, 0) # OK
v2 = (0, "", "", "", 3.14) # OK

Pyright has had support for this for over a year. Mypy just recently added support (in 1.8.0).

If you want to use this in versions of Python prior to 3.11, you’ll need to use Unpack rather than *.

from typing_extensions import Unpack
v1: tuple[int, str, Unpack[tuple[Any, ...]], list[Any]]

Code sample in pyright playground
Code sample in mypy playground


Thank you @erictraut :nerd_face: