Skip to content

[ty] Homogeneous and mixed tuples #18600

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 65 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
14cccc0
fixed-length
dcreager Jun 6, 2025
6602a88
variable
dcreager Jun 6, 2025
c5545df
don't require either
dcreager Jun 9, 2025
0a84630
use variable tuples
dcreager Jun 9, 2025
5b6ab9f
use tupletype for homogeneous tuples
dcreager Jun 10, 2025
f62d536
variable-length isn't gradual
dcreager Jun 10, 2025
3e0e0d8
remove unused stuff
dcreager Jun 10, 2025
d7fe320
more tests
dcreager Jun 10, 2025
9a8b715
use todo_type instead of diagnostic
dcreager Jun 10, 2025
216cabe
return unknown not error
dcreager Jun 10, 2025
3f71700
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 10, 2025
f1bb6c0
todo about comparing variable-length tuples
dcreager Jun 10, 2025
22d5092
more tests
dcreager Jun 10, 2025
bd91962
revert legacy_generic_class_context change
dcreager Jun 10, 2025
ca7d0bf
Add a bunch of subtyping/assignability tests
dcreager Jun 10, 2025
4f03416
clippy
dcreager Jun 10, 2025
c36a145
mdlint
dcreager Jun 10, 2025
2e41487
support indexing into variable-length tuples
dcreager Jun 10, 2025
76d3c1d
fix tests
dcreager Jun 10, 2025
b7f9252
fix project panic
dcreager Jun 10, 2025
b3d83bc
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 10, 2025
d3a0473
Include todo in tuple
dcreager Jun 10, 2025
5334cc8
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 12, 2025
b5bc037
Fix merge conflicts
dcreager Jun 12, 2025
d6711bd
disable mypy in ecosystem check
dcreager Jun 12, 2025
8eac6be
instantiate tuple class to TupleType
dcreager Jun 12, 2025
32a52bf
not for instances too, I guess?
dcreager Jun 12, 2025
a4755e0
mdlint
dcreager Jun 12, 2025
b69a00c
remove fwomp!!!
dcreager Jun 12, 2025
f7f302d
Use vec instead of smallvec
dcreager Jun 12, 2025
14e225a
don't expand variable-length tuples during overload resolution
dcreager Jun 12, 2025
187af5b
track (summarized for now) tuple elements in generic alias
dcreager Jun 12, 2025
0a7f6f1
track tuple spec in specialization
dcreager Jun 16, 2025
604b484
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 16, 2025
9a50239
fix tests
dcreager Jun 16, 2025
4614fa6
update mdtest comment
dcreager Jun 16, 2025
1c029b6
consider tuple spec when checking assignability
dcreager Jun 16, 2025
2bad8f2
todo: nominal instance when instantiating tuple; include suffix when …
dcreager Jun 16, 2025
0f47e9e
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 19, 2025
458ddce
rename tuple constructors
dcreager Jun 19, 2025
5de4fdb
remove homogeneous_supertype
dcreager Jun 19, 2025
5bc1bad
fix equivalence
dcreager Jun 19, 2025
d2bea1b
clean up the diff
dcreager Jun 19, 2025
6a81c7f
todo: check suffix/prefix, Unpack special form
dcreager Jun 19, 2025
2c0ed94
regen
dcreager Jun 19, 2025
7a059d7
index into suffix
dcreager Jun 19, 2025
ef0cf0e
index into prefix
dcreager Jun 19, 2025
45cd117
clippy
dcreager Jun 19, 2025
94ab866
Apply suggestions from code review
dcreager Jun 20, 2025
0aff62e
typo
dcreager Jun 19, 2025
ec873e7
remove `tuple_of`
dcreager Jun 20, 2025
4901b22
explain display comment better
dcreager Jun 20, 2025
90463fa
rename elements methods
dcreager Jun 20, 2025
3cf5944
rename nominal instance disjoint
dcreager Jun 20, 2025
8406319
describe tuple specs better
dcreager Jun 20, 2025
18d7e88
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 20, 2025
f11f8a5
fix variable <: fixed checks
dcreager Jun 20, 2025
ae1a844
add tuple length tests
dcreager Jun 20, 2025
c9a4edd
mdlint
dcreager Jun 20, 2025
233e0a4
add tests for instantiating via `tuple()`
dcreager Jun 20, 2025
7faaba3
add link to spec for gradual tuple type
dcreager Jun 20, 2025
1d7a614
add links to tuple spec
dcreager Jun 20, 2025
c52c845
add generic tuple inference todo tests
dcreager Jun 20, 2025
a10bf50
add note about homogeneous disjointness
dcreager Jun 20, 2025
e5aa429
Merge branch 'main' into dcreager/tuple-spec
dcreager Jun 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 57 additions & 57 deletions crates/ty/docs/rules.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ reveal_type(c) # revealed: tuple[str, int]
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
reveal_type(e) # revealed: tuple[str, ...]

reveal_type(f) # revealed: @Todo(PEP 646)
reveal_type(f) # revealed: tuple[str, *tuple[int, ...], bytes]
reveal_type(g) # revealed: @Todo(PEP 646)

reveal_type(h) # revealed: tuple[list[int], list[int]]
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/resources/mdtest/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -1722,7 +1722,7 @@ d = True
reveal_type(d.__class__) # revealed: <class 'bool'>

e = (42, 42)
reveal_type(e.__class__) # revealed: <class 'tuple'>
reveal_type(e.__class__) # revealed: <class 'tuple[Literal[42], Literal[42]]'>

def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(a.__class__) # revealed: type[int]
Expand Down
28 changes: 27 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/binary/tuples.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]):

```py
def _(x: tuple[int, ...], y: tuple[str, ...]):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(x + (1, 2)) # revealed: tuple[int, ...]
reveal_type((1, 2) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + (3, 4)) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
Comment on lines +22 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so cool!!

```

We get the same results even when we use a legacy type alias, even though this involves first
inferring the `tuple[...]` expression as a value form. (Doing so gives a generic alias of the
`tuple` type, but as a special case, we include the full detailed tuple element specification in
specializations of `tuple`.)

```py
from typing import Literal

OneTwo = tuple[Literal[1], Literal[2]]
ThreeFour = tuple[Literal[3], Literal[4]]
IntTuple = tuple[int, ...]
StrTuple = tuple[str, ...]

def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(one_two + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + three_four) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + x + three_four) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + y + three_four + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
```
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,44 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
```

## Inferring tuple parameter types

```toml
[environment]
python-version = "3.12"
```

```py
from typing import TypeVar

T = TypeVar("T")

def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]

# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown

def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]

# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown

def takes_fixed_tuple(x: tuple[T, int]) -> T:
return x[0]

reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]

def takes_homogeneous_tuple(x: tuple[T, ...]) -> T:
return x[0]

# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```

## Inferring a bound typevar

<!-- snapshot-diagnostics -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,35 @@ reveal_type(takes_in_protocol(ExplicitSub())) # revealed: int
reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
```

## Inferring tuple parameter types

```py
def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]

# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown

def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]

# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown

def takes_fixed_tuple[T](x: tuple[T, int]) -> T:
return x[0]

reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]

def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T:
return x[0]

# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```

## Inferring a bound typevar

<!-- snapshot-diagnostics -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,9 @@ def _(t1: tuple[int | None, int | None], t2: tuple[int, int] | tuple[None, None]
reveal_type(t1[1]) # revealed: int | None

if t2[0] is not None:
reveal_type(t2[0]) # revealed: int
# TODO: should be int
reveal_type(t2[0]) # revealed: Unknown & ~None
# TODO: should be int
reveal_type(t2[1]) # revealed: Unknown
reveal_type(t2[1]) # revealed: int | None
```

### String subscript
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,12 @@ def _(a: tuple[str, int] | tuple[int, str], c: C[Any]):
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
# TODO: Should be `str`
reveal_type(a[1]) # revealed: Unknown
reveal_type(a[1]) # revealed: str | int

if reveal_type(is_int(a[0])): # revealed: TypeIs[int @ a[0]]
# TODO: Should be `tuple[int, str]`
reveal_type(a) # revealed: tuple[str, int] | tuple[int, str]
reveal_type(a[0]) # revealed: Unknown & int
reveal_type(a[0]) # revealed: int

# TODO: Should be `TypeGuard[str @ c.v]`
if reveal_type(guard_str(c.v)): # revealed: @Todo(`TypeGuard[]` special form)
Expand Down
77 changes: 74 additions & 3 deletions crates/ty_python_semantic/resources/mdtest/subscript/tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,64 @@ def _(m: int, n: int):
t[::0] # error: [zero-stepsize-in-slice]

tuple_slice = t[m:n]
# TODO: Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
reveal_type(tuple_slice) # revealed: tuple[Unknown, ...]
reveal_type(tuple_slice) # revealed: tuple[Literal[1, "a", b"b"] | None, ...]
```

## Slices of homogeneous and mixed tuples

```toml
[environment]
python-version = "3.11"
```

```py
from typing import Literal

def homogeneous(t: tuple[str, ...]) -> None:
reveal_type(t[0]) # revealed: str
reveal_type(t[1]) # revealed: str
reveal_type(t[2]) # revealed: str
reveal_type(t[3]) # revealed: str

reveal_type(t[-1]) # revealed: str
reveal_type(t[-2]) # revealed: str
reveal_type(t[-3]) # revealed: str
reveal_type(t[-4]) # revealed: str

def mixed(s: tuple[str, ...]) -> None:
t = (1, 2, 3) + s + (8, 9, 10)

reveal_type(t[0]) # revealed: Literal[1]
reveal_type(t[1]) # revealed: Literal[2]
reveal_type(t[2]) # revealed: Literal[3]
reveal_type(t[3]) # revealed: str | Literal[8]
reveal_type(t[4]) # revealed: str | Literal[8, 9]
reveal_type(t[5]) # revealed: str | Literal[8, 9, 10]

reveal_type(t[-1]) # revealed: Literal[10]
reveal_type(t[-2]) # revealed: Literal[9]
reveal_type(t[-3]) # revealed: Literal[8]
reveal_type(t[-4]) # revealed: Literal[3] | str
reveal_type(t[-5]) # revealed: Literal[2, 3] | str
reveal_type(t[-6]) # revealed: Literal[1, 2, 3] | str
```

## `tuple` as generic alias

For tuple instances, we can track more detailed information about the length and element types of
the tuple. This information carries over to the generic alias that the tuple is an instance of.

```py
def _(a: tuple, b: tuple[int], c: tuple[int, str], d: tuple[int, ...]) -> None:
reveal_type(a) # revealed: tuple[Unknown, ...]
reveal_type(b) # revealed: tuple[int]
reveal_type(c) # revealed: tuple[int, str]
reveal_type(d) # revealed: tuple[int, ...]

reveal_type(tuple) # revealed: <class 'tuple'>
reveal_type(tuple[int]) # revealed: <class 'tuple[int]'>
reveal_type(tuple[int, str]) # revealed: <class 'tuple[int, str]'>
reveal_type(tuple[int, ...]) # revealed: <class 'tuple[int, ...]'>
```

## Inheritance
Expand All @@ -83,8 +139,13 @@ python-version = "3.9"
```py
class A(tuple[int, str]): ...

# revealed: tuple[<class 'A'>, <class 'tuple[@Todo(Generic tuple specializations), ...]'>, <class 'Sequence[@Todo(Generic tuple specializations)]'>, <class 'Reversible[@Todo(Generic tuple specializations)]'>, <class 'Collection[@Todo(Generic tuple specializations)]'>, <class 'Iterable[@Todo(Generic tuple specializations)]'>, <class 'Container[@Todo(Generic tuple specializations)]'>, typing.Protocol, typing.Generic, <class 'object'>]
# revealed: tuple[<class 'A'>, <class 'tuple[int, str]'>, <class 'Sequence[int | str]'>, <class 'Reversible[int | str]'>, <class 'Collection[int | str]'>, <class 'Iterable[int | str]'>, <class 'Container[int | str]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(A.__mro__)

class C(tuple): ...

# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(C.__mro__)
```

## `typing.Tuple`
Expand All @@ -109,9 +170,19 @@ def _(c: Tuple, d: Tuple[int, A], e: Tuple[Any, ...]):
Inheriting from `Tuple` results in a MRO with `builtins.tuple` and `typing.Generic`. `Tuple` itself
is not a class.

```toml
[environment]
python-version = "3.9"
```

```py
from typing import Tuple

class A(Tuple[int, str]): ...

# revealed: tuple[<class 'A'>, <class 'tuple[int, str]'>, <class 'Sequence[int | str]'>, <class 'Reversible[int | str]'>, <class 'Collection[int | str]'>, <class 'Iterable[int | str]'>, <class 'Container[int | str]'>, typing.Protocol, typing.Generic, <class 'object'>]
reveal_type(A.__mro__)

class C(Tuple): ...

# revealed: tuple[<class 'C'>, <class 'tuple[Unknown, ...]'>, <class 'Sequence[Unknown]'>, <class 'Reversible[Unknown]'>, <class 'Collection[Unknown]'>, <class 'Iterable[Unknown]'>, <class 'Container[Unknown]'>, typing.Protocol, typing.Generic, <class 'object'>]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ def _(p: P, q: Q):
assert_type((p, q), tuple[P, Q])
```

## Instantiating tuples

Like all classes, tuples can be instantiated by invoking the `tuple` class. When instantiating a
specialization of `tuple` we (TODO: should) check that the values passed in match the element types
defined in the specialization.

```py
# TODO: revealed: tuple[()]
reveal_type(tuple()) # revealed: tuple[Unknown, ...]
# TODO: revealed: tuple[Literal[1]]
reveal_type(tuple([1])) # revealed: tuple[Unknown, ...]
reveal_type(tuple[int]([1])) # revealed: tuple[int]
# TODO: error for invalid arguments
reveal_type(tuple[int, str]([1])) # revealed: tuple[int, str]

reveal_type(().__class__()) # revealed: tuple[()]
# TODO: error for invalid arguments
reveal_type((1,).__class__()) # revealed: tuple[Literal[1]]
# TODO: error for invalid arguments
reveal_type((1, 2).__class__()) # revealed: tuple[Literal[1], Literal[2]]
```

## Subtyping relationships

The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1`
Expand Down Expand Up @@ -60,10 +82,7 @@ class AnotherEmptyTuple(tuple[()]): ...

static_assert(not is_equivalent_to(AnotherEmptyTuple, tuple[()]))

# TODO: These should not be errors
# error: [static-assert-error]
static_assert(is_subtype_of(AnotherEmptyTuple, tuple[()]))
# error: [static-assert-error]
static_assert(is_assignable_to(AnotherEmptyTuple, tuple[()]))
```

Expand Down Expand Up @@ -158,8 +177,6 @@ class NotAlwaysTruthyTuple(tuple[int]):
def __bool__(self) -> bool:
return False

# TODO: This assignment should be allowed
# error: [invalid-assignment]
t: tuple[int] = NotAlwaysTruthyTuple((1,))
```

Expand Down
Loading
Loading