**Reply to Guido**

> Why do you care about the result of unpacking?

Thinking this through...I guess my main concern would be the issue that I think Eric brought up, concatenation. I'm reasonably convinced it could be done in a way that was sound, but it would be another detail to add to the PEP, which I'm trying to keep minimal.

Also, now that I think about it, if we did allow concatenation, I'm not sure the ellipsis notation would remain intuitive: if we had e.g. `Tuple[int, ..., str]`, my personal intuition is that it would mean "An `int`, zero of more arbitrary types, then a `str`" rather than "An arbitrary number of `int`s, then a `str`".

> ISTM that you can define all important operations on this just fine

Eric pointed out at https://mail.python.org/archives/list/typing-sig@python.org/message/25INUZ5KPEKG6V62VBNHDKVSTHXSEABR/ that we'd need 2 rules: if `Ts == Tuple[T, ...]`, then `Union[*Ts]` collapses to `T`, whereas if `Ts == Tuple[T1, T2, T3]` then `Union[*Ts]` collapses to a union of the individual subtypes with literals stripped. Here too, although this seems sound, it's more just that I don't want to add too many of these kinds of subtle details to at least this initial PEP.

> Did you just change the subject?

Oh, sorry, I see how that was unclear. The connection was that, if we did allow 'open-ended' type variables, then it's possible for Eric's example

def foo(a: Tuple[*Ts], b: Tuple[*Ts]): ...

foo((3, "hi"), ("hi", ))


to type-check fine with `Ts == Tuple[int | str, ...]`.

> but I'm basically arguing that the tuples need to have the same length

Cool, I agree :)

> Or (as I said last night) we could return NoReturn.

*shrug* Sounds fine.

> But see my remark above -- I think it could use `Tuple[object, object]` or perhaps a tuple of two unions.

Oh, I see what you mean.

I'm not sure how to phrase this - I'm a bit of my depth when it comes to constraint-solving - but what I'd intuitively expect to happen is for type-checker to see the first argument, bind `Ts` to `Tuple[int, str]`, then see the second argument, realise its type is different than what `Ts` has already been bound to, and throw an error. Is it possible to set things up like this? Or are there reasons that in general we should try and solve for the most general type possible?

Actually, gosh, this is really worth clarifying. In your example:

def foo(a: T, b: T): ...

foo(3, "hi")  # T becomes object

I see what you mean - that `T` being `object` is a valid solution to this - but at the same time it horrifies me - it definitely doesn't feel like that should be a solution. If I had written that code, I'd be trying to enforce that `a` and `b` are the same type. It looks like pytype complains about this example, but Mypy doesn't (`reveal_type` says `a` and `b` are both 'T`-1'; not sure what that means).

**Eric/Pradeep**, what's the right way to phrase this in the PEP? Is the intuitive behaviour I'm gesturing to what "solving for the individual element of the tuple independently" means?

On Wed, 27 Jan 2021 at 00:56, Guido van Rossum <guido@python.org> wrote:
On Tue, Jan 26, 2021 at 1:29 PM Matthew Rahtz via Typing-sig <typing-sig@python.org> wrote:

That is, should it be possible to bind `Ts` to e.g. `Tuple[int, ...]`?

Guido, you're right: Eric raised this question in the thread at https://mail.python.org/archives/list/typing-sig@python.org/thread/SQVTQYWIOI4TIO7NNBTFFWFMSMS2TA4J.

That links to a whole thread. I trust that Eric asked about this somewhere down the thread. :-)
I originally said no, because it seemed too complicated: a) In general, what would the result of unpacking `Ts` be? b) It introduces extra complications to the behaviour of `Union[*Ts]` - see the thread for more details.

Why do you care about the result of unpacking? That seems to be an artifact of how you implement type checking. But isn't type checking all about manipulating abstractions? ISTM that you can define all important  operations on this just fine (Union[*Ts] would be Union[int, int, int, . . .] which is clearly int).
But now I'm wondering. It seems pretty crucial that `Ts` should be bound to `Tuple[Any, ...]` when no type parameters are specified (i.e. `class Tensor(Generic[*Shape]): ...; Tensor == Tensor[Any, ...]`). There's also Guido's argument that

def foo(*args: *T) -> Tuple[*T]:
    return args
def bar(*x: *int):
    y = foo(*x)


should be perfectly fine at runtime, so it would be weird if the type checker disallowed it.

Then again, in Eric's example:

def foo(a: Tuple[*Ts], b: Tuple[*Ts]): ...

foo((3, "hi"), ("hi", ))

My intuition is strongly that this should be an error. Maybe I'm saying this just because I care most about the numerical computing use-case?

Did you just change the subject? That (Eric's) example doesn't seem to have anything to do with Tuple[int, ...]. And it is no stranger than this:

def foo(a: T, b: T): ...

foo(3, "hi")  # T becomes object

def both_arguments_must_have_same_rank(x1: Tensor[*Shape], x2: Tensor[*Shape]) -> Tensor[*Shape]: ...

x1: Tensor[Batch]
x2: Tensor[Time, Batch]
both_arguments_must_have_same_rank(x1, x2)  # NOOOOOO

I thought I explained that in one of my messages last night:

I'd prefer it if I could think of e.g. `def foo(a: Tuple[Ts], b: Tuple[Ts])` as a series of overloads including `def foo(a: Tuple[T1, T2], b: Tuple[T1, T2])`. That should answer the question, right? Ts stands for `(T1, T2, …, Tn)` for some n (we seem to have an issue about whether n can be zero).  If different checkers produce different answers for the latter, e.g. due to different attitudes about unions, that's okay, but checkers should be consistent with themselves.
Okay, reading that back it's less clear than I remembered it, but I'm basically arguing that the tuples need to have the same length. (And that should be provable, so e.g. with concatenation we agree that `[int, *Ts] == [int, *Ts]` but we treat `[int, *Ts1]` and `[int, *Ts2]` as different.)

So overall I lean towards saying, no, we shouldn't allow it - `Tuple[Any, ...]` is the single exception. (Anyway, if we do find use-cases for open-endedness which are important, we can add it in a later PEP, right?)

Sure, so I'm okay if you add words to the PEP explicitly stating that we're ruling out such cases because they appear too complicated to implement.
**Specific questions**

I'm also collecting everyone's responses to these at https://docs.google.com/document/d/1MhVBFRtqVSnFqpeEu9JmZSohZE1mELOw04wcObwT1LE so we have a central point of reference for all the arguments relevant to each question. I'll also clarify these in the PEP.

> In the expression `x[A, *B]`, what value will be passed to the `__getitem__` method for instance `x`? Will the runtime effectively replace `*B` with `typing.Unpack[B]`?

Given that we're intending `Unpack` to be a stopgap, I'd feel uncomfortable relying on it. My first instinct would be to pass the `TypeVarTuple` instance with an attribute `B._unpacked = True`. That would preserve the most information, giving `x` access to everything inside the `TypeVarTuple`.

I would recommend creating a helper class (at runtime) that wraps the original TypeVar. Here's a prototype (with a dummy class TypeVar):
class TypeVar:
    def __init__(self, name):
        self.name = name

    def __iter__(self):
        yield TypeVarIter(self)

class TypeVarIter:
    def __init__(self, tv):
        self.tv = tv
    def __repr__(self):
        return f"*{self.tv.name}"

Ts = TypeVar("Ts")
a = tuple[(int, *Ts)]
print(a)  # tuple[int, *Ts]

> What is the type of `def foo(*args: Ts) -> Union[Ts]` if `foo` is called with no arguments? In other words, what is the type of `Union[*()]`? Is it `Any`? Is this considered an error?

This should be an error, following the behaviour of `Union` at runtime (try doing `Union[()]`).

Or (as I said last night) we could return NoReturn. (Honestly I think Union[()] could return that too -- I believe that the typing.py module is overzealous in the amount of error checking it is doing.)
> When the constraint solver is solving for a variadic type variable, does it need to solve for the individual elements of the tuple independently? Consider, for example, `def foo(a: Tuple[Ts], b: Tuple[Ts]) -> Tuple[Ts]`. Now, let's consider the expression `foo((3, "hi"), ("hi", 5.6))`? Would this be an error? Or would you expect that the constraint solver produce an answer of `Tuple[int | str, str | float]` (or `Tuple[object, object]`)? It's much easier to implement if we can treat this as an error, but I don't know if that satisfies the use cases you have in mind.

I think this should be an error, so that my `both_arguments_must_have_same_rank` example works.

But see my remark above -- I think it could use Tuple[object, object] or perhaps a tuple of two unions.

--Guido van Rossum (python.org/~guido)