Proposal: Allow Typevartuple unpacking in unions

Various uses of this, including 2 which showed up in Pyright’s change to move support for this behind a flag for experimental features have emerged. Support for this was originally part of pep 646, but was removed as part of the last edit made to it, which was done after it was accepted (With the removals noted in the acceptance)

  • Union[*Ts] should be a supported construct so that typevar tuples behave identically across places where type vars are accepted.
  • Union should not be considered deprecated, only considered as not what people should reach for first.
  • This also resolves a situation where even basic things like Iterating over a generic user-defined heterogeneous container can’t have the type of the iterator be typed properly.

Followup from, and some surrounding context here, there has also been discussion of this not existing as negatively impacting people in the typing channel of the python discord.

An Implementation for this already exists in Pyright, but is currently behind a flag, and is slated for potential future removal if this remains not part of the spec.

6 Likes

An example of a function that requires Union[*Ts] is a random choice between multiple items of potentially different types:

import random
from typing import Union, Never, assert_type

def rng[*Ts](*elements: *Ts) -> Union[*Ts]:
    return random.choice(elements)

x: bool = True
y: int = 1
z: str = "1"

assert_type(rng(x, y, z), bool | int | str)
assert_type(rng(x, y), bool | int)
assert_type(rng(x), bool)
assert_type(rng(), Never)
2 Likes

Absolutely agree with this, however at least in the meantime, or if this proposal is rejected, it is possible to work around this (at least if using Pyright). Using regular type variables with different types in Pyright makes the inferred type of the variable the union of all the types it was used for (which is not true in mypy, where the behaviour generally seems to be to find the last common ancestor but is inconsistent). This means many of these pick-one-from-mixed-types situations can use normal type variables, e.g. for the example above:

def rng[T](*elements: T) -> T:
    return random.choice(elements)

has the desired behaviour. It is even possible to implement this in classes which use a TypeVarTuple, although it’s extremely ugly:

from __future__ import annotations
from typing import Self

class ExampleIterator[*Ts]:
    def __init__(self, given: tuple[*Ts]) -> None:
        self.given = given
        self.it = iter(given)
    
    def __iter__(self) -> Self:
        return self

    def __next__[T](self: ExampleIterator[*tuple[T, ...]]) -> T:
        return next(self.it)

Pyright will now correctly infer the equivalent of Union[*Ts]. Of course this is very verbose and unclear compared to the desired syntax