From 6ae18f00d3fcb7619f136fe2bf22c01d1119126d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 30 Jun 2026 20:50:12 +0100 Subject: [PATCH 1/7] Add test + some experiments --- mypy/subtypes.py | 25 ++++++++++++++++++------- test-data/unit/check-typevar-tuple.test | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 2d97dd534d9f..4f14c27af8cd 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -509,18 +509,19 @@ def visit_instance(self, left: Instance) -> bool: if isinstance(unpacked, Instance): return self._is_subtype(left, unpacked) if left.type.has_base(right.partial_fallback.type.fullname): + mapped = map_instance_to_supertype(left, right.partial_fallback.type) + # print("MMMM", left, right, mapped) if not self.proper_subtype: # Special cases to consider: # * Plain tuple[Any, ...] instance is a subtype of all tuple types. # * Foo[*tuple[Any, ...]] (normalized) instance is a subtype of all # tuples with fallback to Foo (e.g. for variadic NamedTuples). - mapped = map_instance_to_supertype(left, right.partial_fallback.type) - if is_erased_instance(mapped): - if ( - mapped.type.fullname == "builtins.tuple" - or mapped.type.has_type_var_tuple_type - ): - return True + if is_erased_instance(mapped) and mapped.type.fullname == "builtins.tuple": + return True + if is_normalized_instance(mapped): + # print("HHHH", mapped, right.partial_fallback) + if self._is_subtype(mapped, right.partial_fallback): + return True return False if isinstance(right, TypeVarTupleType): # tuple[Any, ...] is like Any in the world of tuples (see special case above). @@ -822,6 +823,7 @@ def visit_tuple_type(self, left: TupleType) -> bool: # doesn't have one, we will fall through to False down the line. if self.variadic_tuple_subtype(left, right): return True + # print("TTT", left.items, right.items) if len(left.items) != len(right.items): return False if any(not self._is_subtype(l, r) for l, r in zip(left.items, right.items)): @@ -2384,3 +2386,12 @@ def is_erased_instance(t: Instance) -> bool: elif not isinstance(get_proper_type(arg), AnyType): return False return True + + +def is_normalized_instance(t: Instance) -> bool: + if not t.args: + return False + if not t.type.tuple_type: + return False + expanded = expand_type_by_instance(t.type.tuple_type, t) + return isinstance(expanded, Instance) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 55f125d1fc03..8477c4679fa6 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2917,3 +2917,20 @@ def g(x: K) -> None: assert d is not None reveal_type(d) # N: Revealed type is "tuple[K | list[int], ...] | tuple[*tuple[int | list[int], ...], int]" [builtins fixtures/tuple.pyi] + +[case testFooBar] +from typing import Any, Generic, Unpack, TypeVar, TypeVarTuple + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") +class Array(tuple[Unpack[Ts]], Generic[T, Unpack[Ts]]): ... + +Float2D = Array[float, int, int] +FloatND = Array[float, *tuple[Any, ...]] + +def from_2d(a: Float2D) -> FloatND: + return a + +def into_2d(a: FloatND) -> Float2D: + return a +[builtins fixtures/tuple.pyi] From b2c4c19daeaae156919336c2ac1a654ec097a2fe Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 16 Sep 2026 12:56:42 +0100 Subject: [PATCH 2/7] Update test --- test-data/unit/check-typevar-tuple.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 8477c4679fa6..53c9098b5554 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2918,7 +2918,7 @@ def g(x: K) -> None: reveal_type(d) # N: Revealed type is "tuple[K | list[int], ...] | tuple[*tuple[int | list[int], ...], int]" [builtins fixtures/tuple.pyi] -[case testFooBar] +[case testVariadicTupleSubtypingWithAnyBasic] from typing import Any, Generic, Unpack, TypeVar, TypeVarTuple T = TypeVar("T") @@ -2926,7 +2926,7 @@ Ts = TypeVarTuple("Ts") class Array(tuple[Unpack[Ts]], Generic[T, Unpack[Ts]]): ... Float2D = Array[float, int, int] -FloatND = Array[float, *tuple[Any, ...]] +FloatND = Array[float, Unpack[tuple[Any, ...]]] def from_2d(a: Float2D) -> FloatND: return a From 76691116786bec686e6a702710af22f0c2727269 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 17 Sep 2026 00:27:57 +0100 Subject: [PATCH 3/7] Add plan --- mypy/subtypes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 4f14c27af8cd..a451c28f7fd6 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -824,6 +824,7 @@ def visit_tuple_type(self, left: TupleType) -> bool: if self.variadic_tuple_subtype(left, right): return True # print("TTT", left.items, right.items) + # TODO: adjust variadic left Any to fixed right (incl. *Ts), if possible. if len(left.items) != len(right.items): return False if any(not self._is_subtype(l, r) for l, r in zip(left.items, right.items)): @@ -916,6 +917,7 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: # subtyping: *each* item on the left, must be a subtype of *some* item on the right. # For this we first check the "asymptotic case", i.e. that both unpacks a subtypes, # and then check subtyping for all finite overlaps. + # TODO: if left_item is Any, use any() logic, not all(). if not self._is_subtype(left_item, right_item): return False max_overlap = max(0, right_prefix - left_prefix, right_suffix - left_suffix) From da025f411a991809255ede955bc9d34fbd508c17 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 17 Sep 2026 01:35:40 +0100 Subject: [PATCH 4/7] Add basic implementation --- mypy/subtypes.py | 68 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a451c28f7fd6..728b169a4b1c 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -67,6 +67,7 @@ UninhabitedType, UnionType, UnpackType, + extend_args_for_prefix_and_suffix, find_unpack_in_list, flatten_nested_unions, get_proper_type, @@ -824,7 +825,7 @@ def visit_tuple_type(self, left: TupleType) -> bool: if self.variadic_tuple_subtype(left, right): return True # print("TTT", left.items, right.items) - # TODO: adjust variadic left Any to fixed right (incl. *Ts), if possible. + left = self.adjust_left_if_possible(left, right) if len(left.items) != len(right.items): return False if any(not self._is_subtype(l, r) for l, r in zip(left.items, right.items)): @@ -844,6 +845,51 @@ def visit_tuple_type(self, left: TupleType) -> bool: else: return False + def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleType: + left_variadic = self.get_variadic_item(left) + if left_variadic is None: + return left + left_unpack_index, left_item = left_variadic + if not isinstance(get_proper_type(left_item), AnyType): + return left + right_unpack_index = find_unpack_in_list(right.items) + if right_unpack_index is None: + if len(left.items) > len(right.items) + 1: + return left + num_anys = len(right.items) - len(left.items) + 1 + return left.copy_modified( + items=left.items[:left_unpack_index] + + [left_item] * num_anys + + left.items[left_unpack_index + 1 :] + ) + right_unpack = right.items[right_unpack_index] + assert isinstance(right_unpack, UnpackType) + right_unpacked = get_proper_type(right_unpack.type) + if isinstance(right_unpacked, Instance): + return left + right_prefix = right_unpack_index + right_suffix = len(right.items) - right_prefix - 1 + left_prefix = left_unpack_index + left_suffix = len(left.items) - left_prefix - 1 + if left_prefix > right_prefix or left_suffix > right_suffix: + return left + new_items = extend_args_for_prefix_and_suffix( + tuple(left.items), right_prefix, right_suffix + ) + return left.copy_modified(items=list(new_items)) + + def get_variadic_item(self, tup: TupleType) -> tuple[int, Type] | None: + unpack_index = find_unpack_in_list(tup.items) + if unpack_index is None: + return None + unpack = tup.items[unpack_index] + assert isinstance(unpack, UnpackType) + unpacked = get_proper_type(unpack.type) + if not isinstance(unpacked, Instance): + return None + assert unpacked.type.fullname == "builtins.tuple" + return unpack_index, unpacked.args[0] + def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: """Check subtyping between two potentially variadic tuples. @@ -853,18 +899,11 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: Note: the cases where right is fixed or has *Ts unpack should be handled by the caller. """ - right_unpack_index = find_unpack_in_list(right.items) - if right_unpack_index is None: + right_variadic = self.get_variadic_item(right) + if right_variadic is None: # This case should be handled by the caller. return False - right_unpack = right.items[right_unpack_index] - assert isinstance(right_unpack, UnpackType) - right_unpacked = get_proper_type(right_unpack.type) - if not isinstance(right_unpacked, Instance): - # This case should be handled by the caller. - return False - assert right_unpacked.type.fullname == "builtins.tuple" - right_item = right_unpacked.args[0] + right_unpack_index, right_item = right_variadic right_prefix = right_unpack_index right_suffix = len(right.items) - right_prefix - 1 left_unpack_index = find_unpack_in_list(left.items) @@ -917,7 +956,7 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: # subtyping: *each* item on the left, must be a subtype of *some* item on the right. # For this we first check the "asymptotic case", i.e. that both unpacks a subtypes, # and then check subtyping for all finite overlaps. - # TODO: if left_item is Any, use any() logic, not all(). + use_any = isinstance(get_proper_type(left_item), AnyType) if not self._is_subtype(left_item, right_item): return False max_overlap = max(0, right_prefix - left_prefix, right_suffix - left_suffix) @@ -927,7 +966,10 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: repr_items += left.items[-left_suffix:] left_repr = left.copy_modified(items=repr_items) if not self._is_subtype(left_repr, right): - return False + if not use_any: + return False + elif use_any: + return True return True def is_top_type(self, typ: Type) -> bool: From 00e7bdfbef3b3d9ce15b209e7bc34ca6e67e353d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 17 Sep 2026 13:06:51 +0100 Subject: [PATCH 5/7] Fix tvt edge case; add tests --- mypy/subtypes.py | 14 ++- test-data/unit/check-typevar-tuple.test | 119 ++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 728b169a4b1c..dbe005d10a90 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -925,16 +925,16 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: return False return all(self._is_subtype(li, right_item) for li in middle) else: - if len(left.items) < len(right.items): - # There are some items on the left that will never have a matching length - # on the right. - return False left_prefix = left_unpack_index left_suffix = len(left.items) - left_prefix - 1 left_unpack = left.items[left_unpack_index] assert isinstance(left_unpack, UnpackType) left_unpacked = get_proper_type(left_unpack.type) if not isinstance(left_unpacked, Instance): + if len(left.items) < len(right.items): + # There are some items on the left that will never have a matching length + # on the right. + return False # *Ts unpack can't be split, except if it is all mapped to Anys or objects. if self.is_top_type(right_item): right_prefix_types, middle, right_suffix_types = split_with_prefix_and_suffix( @@ -957,6 +957,10 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: # For this we first check the "asymptotic case", i.e. that both unpacks a subtypes, # and then check subtyping for all finite overlaps. use_any = isinstance(get_proper_type(left_item), AnyType) + if not use_any and len(left.items) < len(right.items): + # There are some items on the left that will never have a matching length + # on the right. + return False if not self._is_subtype(left_item, right_item): return False max_overlap = max(0, right_prefix - left_prefix, right_suffix - left_suffix) @@ -970,7 +974,7 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: return False elif use_any: return True - return True + return not use_any def is_top_type(self, typ: Type) -> bool: if not self.proper_subtype and isinstance(get_proper_type(typ), AnyType): diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test index 53c9098b5554..a8898a8329be 100644 --- a/test-data/unit/check-typevar-tuple.test +++ b/test-data/unit/check-typevar-tuple.test @@ -2919,6 +2919,74 @@ def g(x: K) -> None: [builtins fixtures/tuple.pyi] [case testVariadicTupleSubtypingWithAnyBasic] +from typing import Any, Unpack + +Ge1 = tuple[int, Unpack[tuple[Any, ...]]] + +def ge1_to_0(shape: Ge1) -> tuple[()]: + return shape # E: Incompatible return value type (got "tuple[int, Unpack[tuple[Any, ...]]]", expected "tuple[()]") +def ge1_to_1(shape: Ge1) -> tuple[int]: + return shape +def ge1_to_2(shape: Ge1) -> tuple[int, int]: + return shape +[builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubtypingWithAnyExtra] +from typing import Any, Unpack + +def foo(x: tuple[int, str]) -> None: ... +def bar(x: tuple[int, int]) -> None: ... + +t1: tuple[int, Unpack[tuple[Any, ...]], str] +t2: tuple[Unpack[tuple[Any, ...]], int, str] +t3: tuple[int, str, Unpack[tuple[Any, ...]]] +t4: tuple[int, Unpack[tuple[Any, ...]], int] +foo(t1) +foo(t2) +foo(t3) +foo(t4) # E: Argument 1 to "foo" has incompatible type "tuple[int, Unpack[tuple[Any, ...]], int]"; expected "tuple[int, str]" + +tt1: tuple[int, Unpack[tuple[Any, ...]], int, int] +tt2: tuple[int, int, Unpack[tuple[Any, ...]], int] +tt3: tuple[Unpack[tuple[Any, ...]], int] +tt4: tuple[Unpack[tuple[Any, ...]], str] +bar(tt1) # E: Argument 1 to "bar" has incompatible type "tuple[int, Unpack[tuple[Any, ...]], int, int]"; expected "tuple[int, int]" +bar(tt2) # E: Argument 1 to "bar" has incompatible type "tuple[int, int, Unpack[tuple[Any, ...]], int]"; expected "tuple[int, int]" +bar(tt3) +bar(tt4) # E: Argument 1 to "bar" has incompatible type "tuple[Unpack[tuple[Any, ...]], str]"; expected "tuple[int, int]" +[builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubtypingWithAnyExtraTypeVar] +from typing import Any, Unpack, TypeVarTuple + +t1: tuple[int, Unpack[tuple[Any, ...]], int] +t2: tuple[int, int, Unpack[tuple[Any, ...]], int, int] +t3: tuple[int, int, int, Unpack[tuple[Any, ...]], int, int, int] + +Ts = TypeVarTuple("Ts") +def test(x: tuple[int, int, Unpack[Ts], int, int]) -> None: + x = t1 + x = t2 + x = t3 # E: Incompatible types in assignment (expression has type "tuple[int, int, int, Unpack[tuple[Any, ...]], int, int, int]", variable has type "tuple[int, int, Unpack[Ts], int, int]") +[builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubtypingWithAnyExtraVariadic] +from typing import Any, Unpack + +def foo(x: tuple[int, Unpack[tuple[int, ...]], int]) -> None: ... + +t1: tuple[int, Unpack[tuple[Any, ...]]] +t2: tuple[Unpack[tuple[Any, ...]], int] +t3: tuple[Unpack[tuple[Any, ...]], str] +t4: tuple[Unpack[tuple[Any, ...]], int, int, int] + +foo(t1) +foo(t2) +foo(t3) # E: Argument 1 to "foo" has incompatible type "tuple[Unpack[tuple[Any, ...]], str]"; expected "tuple[int, Unpack[tuple[int, ...]], int]" +foo(t4) +[builtins fixtures/tuple.pyi] + +[case testVariadicTupleSubclassSubtypingWithAny] from typing import Any, Generic, Unpack, TypeVar, TypeVarTuple T = TypeVar("T") @@ -2934,3 +3002,54 @@ def from_2d(a: Float2D) -> FloatND: def into_2d(a: FloatND) -> Float2D: return a [builtins fixtures/tuple.pyi] + +[case testVariadicInstanceSubtypingWithAny] +from typing import Any, Generic, Unpack, TypeVarTuple + +Ts = TypeVarTuple("Ts") +class C(Generic[Unpack[Ts]]): ... + +def foo(x: C[int, str]) -> None: ... +def bar(x: C[int, int]) -> None: ... +def baz(x: C[int, int, int]) -> None: ... + +t1: C[int, Unpack[tuple[Any, ...]], str] +t2: C[Unpack[tuple[Any, ...]], int, str] +t3: C[int, str, Unpack[tuple[Any, ...]]] +t4: C[int, Unpack[tuple[Any, ...]], int] +foo(t1) +foo(t2) +foo(t3) +foo(t4) # E: Argument 1 to "foo" has incompatible type "C[int, Unpack[tuple[Any, ...]], int]"; expected "C[int, str]" + +tt1: C[int, Unpack[tuple[Any, ...]], int, int] +tt2: C[int, int, Unpack[tuple[Any, ...]], int] +tt3: C[Unpack[tuple[Any, ...]], int] +tt4: C[Unpack[tuple[Any, ...]], str] +bar(tt1) # E: Argument 1 to "bar" has incompatible type "C[int, Unpack[tuple[Any, ...]], int, int]"; expected "C[int, int]" +bar(tt2) # E: Argument 1 to "bar" has incompatible type "C[int, int, Unpack[tuple[Any, ...]], int]"; expected "C[int, int]" +bar(tt3) +bar(tt4) # E: Argument 1 to "bar" has incompatible type "C[Unpack[tuple[Any, ...]], str]"; expected "C[int, int]" + +baz(tt3) +baz(t4) +[builtins fixtures/tuple.pyi] + +[case testVariadicInstanceSubtypingWithAnyVariadic] +from typing import Any, Generic, Unpack, TypeVarTuple + +Ts = TypeVarTuple("Ts") +class C(Generic[Unpack[Ts]]): ... + +def foo(x: C[int, Unpack[tuple[int, ...]], int]) -> None: ... + +t1: C[int, Unpack[tuple[Any, ...]]] +t2: C[Unpack[tuple[Any, ...]], int] +t3: C[Unpack[tuple[Any, ...]], str] +t4: C[Unpack[tuple[Any, ...]], int, int, int] + +foo(t1) +foo(t2) +foo(t3) # E: Argument 1 to "foo" has incompatible type "C[Unpack[tuple[Any, ...]], str]"; expected "C[int, Unpack[tuple[int, ...]], int]" +foo(t4) +[builtins fixtures/tuple.pyi] From 134a763f59644ac0dce63afbba8749486592e308 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 17 Sep 2026 13:13:31 +0100 Subject: [PATCH 6/7] Delete debug prints --- mypy/subtypes.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index dbe005d10a90..a257fb611b19 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -511,7 +511,6 @@ def visit_instance(self, left: Instance) -> bool: return self._is_subtype(left, unpacked) if left.type.has_base(right.partial_fallback.type.fullname): mapped = map_instance_to_supertype(left, right.partial_fallback.type) - # print("MMMM", left, right, mapped) if not self.proper_subtype: # Special cases to consider: # * Plain tuple[Any, ...] instance is a subtype of all tuple types. @@ -520,7 +519,6 @@ def visit_instance(self, left: Instance) -> bool: if is_erased_instance(mapped) and mapped.type.fullname == "builtins.tuple": return True if is_normalized_instance(mapped): - # print("HHHH", mapped, right.partial_fallback) if self._is_subtype(mapped, right.partial_fallback): return True return False @@ -824,7 +822,6 @@ def visit_tuple_type(self, left: TupleType) -> bool: # doesn't have one, we will fall through to False down the line. if self.variadic_tuple_subtype(left, right): return True - # print("TTT", left.items, right.items) left = self.adjust_left_if_possible(left, right) if len(left.items) != len(right.items): return False From e1898e89470dca5fb4c40223defdd06fa87dfc22 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 17 Sep 2026 13:54:37 +0100 Subject: [PATCH 7/7] Add/adjust comments/docstring --- mypy/subtypes.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index a257fb611b19..e9060ae845cc 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -511,11 +511,11 @@ def visit_instance(self, left: Instance) -> bool: return self._is_subtype(left, unpacked) if left.type.has_base(right.partial_fallback.type.fullname): mapped = map_instance_to_supertype(left, right.partial_fallback.type) + # Special cases to consider: + # * tuple[Any, ...] instance is a (non-proper) subtype of all tuple types. + # * Foo[*tuple[X, ...]] (normalized) instance is a subtype of all + # tuples with appropriate fallback (e.g. for variadic NamedTuples). if not self.proper_subtype: - # Special cases to consider: - # * Plain tuple[Any, ...] instance is a subtype of all tuple types. - # * Foo[*tuple[Any, ...]] (normalized) instance is a subtype of all - # tuples with fallback to Foo (e.g. for variadic NamedTuples). if is_erased_instance(mapped) and mapped.type.fullname == "builtins.tuple": return True if is_normalized_instance(mapped): @@ -819,9 +819,11 @@ def visit_tuple_type(self, left: TupleType) -> bool: elif isinstance(right, TupleType): # If right has a variadic unpack this needs special handling. If there is a TypeVarTuple # unpack, item count must coincide. If the left has variadic unpack but right - # doesn't have one, we will fall through to False down the line. + # doesn't have one, we will fall through. if self.variadic_tuple_subtype(left, right): return True + # The only case where variadic can be subtype of fixed is when left variadic item is Any. + # Otherwise, the original left will be returned, causing fall through to False. left = self.adjust_left_if_possible(left, right) if len(left.items) != len(right.items): return False @@ -843,6 +845,11 @@ def visit_tuple_type(self, left: TupleType) -> bool: return False def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleType: + """Adjust shape of left containing *tuple[Any, ...] to match right. + + Note: this only works if right is fixed size (including *Ts), the variadic + right are handled by the caller, currently with variadic_tuple_subtype(). + """ left_variadic = self.get_variadic_item(left) if left_variadic is None: return left @@ -876,6 +883,7 @@ def adjust_left_if_possible(self, left: TupleType, right: TupleType) -> TupleTyp return left.copy_modified(items=list(new_items)) def get_variadic_item(self, tup: TupleType) -> tuple[int, Type] | None: + """If this is tuple[X, *tuple[Y, ...], Z], return Y, otherwise None.""" unpack_index = find_unpack_in_list(tup.items) if unpack_index is None: return None @@ -953,6 +961,7 @@ def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool: # subtyping: *each* item on the left, must be a subtype of *some* item on the right. # For this we first check the "asymptotic case", i.e. that both unpacks a subtypes, # and then check subtyping for all finite overlaps. + # Note: if the left item is Any we use any() semantics instead of all(). use_any = isinstance(get_proper_type(left_item), AnyType) if not use_any and len(left.items) < len(right.items): # There are some items on the left that will never have a matching length @@ -2434,6 +2443,15 @@ def is_erased_instance(t: Instance) -> bool: def is_normalized_instance(t: Instance) -> bool: + """Is this instance type a normalized representation of a tuple type? + + Type like class C[T, *Ts](tuple[T, *Ts]) are internally represented as + tuple types, so that e.g. C[int, str] is tuple[int, str, fallback=C[int, str]]. + However, in case of a variadic argument they are normalized, similar to how + tuple[*tuple[int, ...]] (TupleType) is normalized to tuple[int, ...] (Instance). + For example, C[int, *tuple[str, ...]] is represented as an instance. This + function detects such instances, when they need special-casing. + """ if not t.args: return False if not t.type.tuple_type: