I think some of this discussion is going in circles because there are two slightly different things that are being treated the same, mostly because our notation (in this case the one provided by the type system) is not expressive enough to describe the problem, so let me give it a try.
I’ll have to introduce some new notation on the way. This IS NOT A PROPOSAL to add that notation as python syntax, it’s just an aid for the discussion.
(Long Contextual Intro starts)
Python allows to be quite specific in types, (e.g. list[int]
), gradual (e.g Any
) and some types that have partial information and some graduality (e.g. dict[str, Any]
).
We normally use Any
to refer to stuff where we developers can figure out the type, but it’s too tough to describe that to the type-checker. I don’t see it as a “sin” or something to avoid, it’s just saying “we’re using Python beyond what the type system allows us to describe”, which can happen because Python is very flexible.
Some people are worried about missing some errors on the approach, and while that happens it’s not the key issue. After all, every typechecker happily accepts:
x: list[int] = [1,2,3]
a, b = x # ValueError on runtime, type checkers accept this
So if we allow this, and no one has complained, what make tuples different? Currently we have that tuple[int, ...]
doesn’t behave that differently from a list:
x: tuple[int, ...] = (1,2,3)
a, b = x # ValueError on runtime, type checkers accept this. Same as before but list -> tuple
a, b, c = x # Perfectly fine on runtime, type checkers accept this (we like when this happens)
What is completely unique about tuples vs other containers is that we can specify a length, and when we have those, the issue on this thread arises. If the type checker allows a = b
where a
has fixed length, and b
has “arbitrary” length, then the code using a
further down the road may be mistyped because type doesn’t match reality (a.k.a. run time types). Furthermore we expect those issues to happen only on gradual types, but this problem could easily happen with tuple[int, int] vs tuple[int, ...]
, where Any
is not involved at all, and that is what makes this “graduality” unexpected.
(Long Contextual Intro ends)
My interpretation of the problem is that the current tuple
type allows us to be gradual on its elements, but not on a very important (and unique) attribute of tuples: its length. If we had a (silly, not a real proposal) way to specify the type of the length, the problem would be crystal clear.
# This is some example of a notation allowing to specify type of tuple lengths:
t1: tuple[str, str, length:int] = "foo", "bar"
t2: tuple[str, str, bool, length:Literal[3]] = "foo", "bar", True
# Most of the current tuple with fixed elements declaration should have a meaning similar
# to t2 rather than t1
t3: tuple[str, ..., length: int] = "foo", "bar", "baz"
# The declaration of t3 is probably similar to the current interpretation of tuple[str, ...]
What does this give us? well, as I said in the Long Contextual Intro, We use Any
mostly for “I know the type even if the type checker doesn’t”. But when we look at length, there are actually two very different scenarios:
- (A) The length is really arbitrary, and the code should handle any possibility. Example:
my_list: list[Any]; x = tuple(my_list)
- (B) The length is not arbitrary, it’s known by the developer but not the type checker. Example:
row = db.fetch_row("SELECT a, b FROM table")
If we can talk about the type of the tuple length, case A
has type tuple[Any, ..., length: int]
. But case B
should have type tuple[Any, ..., length: Any]
. That would make it clear that
my_list: list[Any] = generate_some_list()
t1: tuple[int, str, length: Literal[2]]
t1 = tuple(my_list) # Wrong! Literal[2] can't accept int
t2: tuple[int, str, length: Literal[2]]
t2 = db.fetch_row("SELECT a, b FROM table") # Fine! Literal[2] can accept Any
t3: tuple[object, ..., length: int] = t2 # Fine! int can accept Literal[2]
Note that the length
component essentially works as just another covariant type argument. And this gives us the rules we want with no special magic.
So this ends my analysis, and hopefully this helps aiming the discussion better. I don’t have a solution
and I’m hoping the smart minds here can come up with something acceptable.
The key issue would be being able to describe what I called tuple[T, ..., length: Any]
, which is not available now. I wouldn’t ask developers on explicitly giving the length type arg, but I wouldn’t mind if it appeared on the spec (that allows to make the semantics clear no matter which scenario we support.). But it would make sense to have tuple
to mean tuple[Any, ..., length: Any]
, because an unqualified generic usually means "every generic arg is Any
, and that would include the length. That would distinguish that type from tuple[Any, ..., length: int]
(which is today’s semantics for tuple[Any, ...]
and most likely what we want for backwards compatibility). However that doesn’t give us a way to write tuple[Foo, ..., length: Any]
which is probably useful.
I hope this opens some new directions in this discussion.