# Unbounded Tuples

I've added support for accepting `Ts` as an unbounded tuple `Tuple[Any, ...]` or `Tuple[int, ...]`. The type inference rules were similar to that in TypeScript (https://github.com/microsoft/TypeScript/pull/39094).

This meant we could do:

```
def foo(xs: Tuple[*Ts]) -> Tuple[*Ts]: ...

def baz() -> None:
unbounded_tuple: Tuple[int, ...]
z = foo(unbounded_tuple)
# => Tuple[int, ...]
reveal_type(z)

def foo2(xs: Tuple[T, *Tuple[str, ...]]) -> T: ...

def baz2() -> None:
some_tuple: Tuple[int, str, str]
z = foo(some_tuple)
# => int
reveal_type(z)
```

I'm ambivalent about allowing such explicit unpacking of `Tuple[int, ...]`. Given that we need it for arbitrary-rank `Tensor` anyway, it seems cleaner to allow it, but it may be confusing for users.

Another thing we may want to consider in a future PEP: setting a bound for `Ts`. For example, we may want Tensor parameters to be bound by int: i.e., allow `Tensor[Literal[480], Literal[360]]` but not `Tensor[str, str]`. This could be done by essentially setting `TypeVarTuple("Ts", bound=Tuple[int, ...])`. This might be useful for future type arithmetic, since we'd need to be able to safely say something like `def flatten(x: Tensor[*Ts]) -> Tensor[Product[Ts]]:`.

# Arbitrary-rank Tensors

For gradual typing, we'd need to allow `x: Tensor`. Some library functions may be using `Tensor` without parameters until they are migrated to variadics. Calling them should not raise errors.

So, I treated `Tensor` without parameters as `Tensor[*Tuple[Any, ...]]`. (As Guido pointed out, `Tensor[Any, ...]` is not valid syntax.)

Gradual typing has two main requirements:

(a) `Tensor[int, str]` should be compatible with `Tensor`

```
def expects_arbitrary_tensor(x: Tensor) -> Tensor: ...

def bar() -> None:
tensor: Tensor[int, str]
y = expects_arbitrary_tensor(tensor)
reveal_type(y)
```

(b) `Tensor` should be compatible with a concrete `Tensor[int, str]`

```python
def expects_concrete_tensor(x: Tensor[int, str]) -> Tensor[int, str]: ...

def bar() -> None:
tensor: Tensor
expects_concrete_tensor(tensor)
```

(This is analogous to `List[Any]` being compatible with `List[int]` and vice versa.)

By default, both raised an error because Tensor is invariant. That is, we had to check that its parameters were compatible in both directions: (a) `[int, str]` is compatible with `[*Tuple[Any, ...]]` and (b) `[*Tuple[Any, ...]]` is compatible with `[int, str]`.

To be explicit, (b) is equivalent to checking that `Tuple[Any, ...]` is compatible with `Tuple[int, str]`. That is a problem because we don't generally consider `Tuple[Any, ...]` to be compatible with `Tuple[int, str]`. For example, Mypy raises an error:

```python
from typing import Any, Tuple

def expects_concrete_tuple(x: Tuple[int, str]) -> None: ...

def bar() -> None:
unbounded_tuple: Tuple[Any, ...]
# main.py:9: error: Argument 1 to "expects_concrete_tuple" has incompatible type "Tuple[Any, ...]"; expected "Tuple[int, str]"
y = expects_concrete_tuple(unbounded_tuple)
reveal_type(y)
```

To work around this, we could either

(i) allow Tuple[Any, ...] in general to be compatible with Tuple[int, str], or

(ii) special-case variadic classes like Tensor so that `Tensor` is compatible with `Tensor[int, str]` and vice versa.

Both are unsound. The tuple or tensor we pass in may have zero elements and may thus cause a runtime error. Or its element may be a type that can't be used as an `int` or `str`, which is again a runtime error.

However, option (ii) is less invasive, so I went with it. The Tensor examples typechecked fine. Let me know if anyone has strong opinions about option (i).

****

I'll add these points to the PEP. I'll work on merging my changes into Pyre master, but this might take a few weeks because I'll have to replace the existing ListVariadic implementation.

On Sun, Jan 31, 2021 at 11:17 AM Matthew Rahtz via Typing-sig <typing-sig@python.org> wrote:
Wow, Eric, that was fast! Thanks for your great work! :)

**PEP draft**: I've updated the current draft of the PEP at https://github.com/python/peps/pull/1781 to reflect the decisions we've made. I think it now more or less reflects the behaviour in Pyright's implementation (minus aliases, which I've yet to rewrite the PEP section for).

One small thing that's different to our discussion is the behaviour of a `Union` of an empty `TypeVarTuple`: I realised that assigning a type of `NoReturn` to such `Union`s would only make sense if the `Union` in question was in a return annotation, so I've stuck with saying that the type-checker should produce an error so that the behaviour is consistent between `Union` in returns and `Union` elsewhere.

> No, that's not how type variables work at all!

Ahh, thanks for clarifying. This was pretty eye-opening. I've tried to make the expected behaviour for `TypeVarTuple` explicit in the current draft of the PEP by saying that we disallow `Tuple[Union[A, B]]`, and that types must match exactly. (I haven't mentioned the 'class hierarchies' case in the draft because we've defined `TypeVarTuple` as invariant for the time being.)

On Sun, 31 Jan 2021 at 01:30, Eric Traut <eric@traut.com> wrote:
I've done a first-cut implementation of PEP 646 in pyright 1.1.107. I just published this version, so you can try it by installing the Pyright extension in VS Code.

Specific notes about my current implementation:
* It supports `TypeVarTuple`, which is exported by the typing_extensions.pyi that ships with pyright.
* It supports `Unpack`, also exported by typing_extensions.pyi.
* It does not currently support `*` syntax, since that will be introduced with PEP 637 functionality.
* It does not allow packed usage of a TypeVarTuple. All uses of a TypeVarTuple must be contained with an `Unpack`, and errors are generated if they are not.
* If a TypeVarTuple appears within a subscript for a type annotation, it must be the last entry (i.e. no suffixes). The one exception is `Union`, which allows it to appear anywhere. I figured this was justified because the order of type arguments within a `Union` are not relevant.
* If a TypeVarTuple appears within a class declaration, only one is allowed, and it must be after all other type variables. The order can be forced by including an explicit `Generic` that defines the type parameter ordering.
* At most one TypeVarTuple can appear within a subscript when specializing a class (e.g. `Tuple[Ts1, Ts2]` is an error). The one exception is `Union`, which allows for multiple TypeVarTuples to appear. This creates an ambiguity for the constraint solver, but this ambiguity already exists for traditional TypeVars.
* Type aliases may contain at most one TypeVarTuple, and it must be after all other TypeVars that parameterize the type alias (e.g. `Alias1 = Union[List[T], Tuple[Unpack[Ts]]]` is allowed, but `Alias2 = Union[Tuple[Unpack[Ts]], List[T]]` is an error. Unlike with class declarations, there's no way to force the ordering of type parameters within a type alias, which is somewhat constraining.
* An attempt to assign an open-ended tuple to a TypeVarTuple during constrain solving results in an error.
* A Callable may include a TypeVarTuple within its parameter type list, but only one is allowed, and it must be in the last entry. Other cases are flags as errors.
* If a TypeVarTuple appears more than once in a function signature, the tuples that are assigned to it must match in both length and in type. There is no attempt to widen the type to accommodate differences.

As I anticipated, this was a very large and complex feature to implement. If you're curious, here's the [commit](https://github.com/microsoft/pyright/commit/1d06018908819e17daa08328a64e6e1d5c68c30c).

I've implemented a bunch of test cases. Perhaps these will be of use for other type checker maintainers as when they add support for this PEP. The test samples can be found [here](https://github.com/microsoft/pyright/blob/master/packages/pyright-internal/src/tests/samples/variadicTypeVar1.py). (There are 8 test files and many dozens of test cases for this feature currently.)

Feedback and bug reports are welcome.

--
Eric Traut
Contributor to Pyright and Pylance
Microsoft Corp.
_______________________________________________
Typing-sig mailing list -- typing-sig@python.org
To unsubscribe send an email to typing-sig-leave@python.org
https://mail.python.org/mailman3/lists/typing-sig.python.org/