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)