Skip to content

tuple[Any, *tuple[Any, ...]] is not assignable to other tuple types #19109

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

Open
jorenham opened this issue May 18, 2025 · 2 comments
Open

tuple[Any, *tuple[Any, ...]] is not assignable to other tuple types #19109

jorenham opened this issue May 18, 2025 · 2 comments
Labels
bug mypy got something wrong topic-pep-646 PEP 646 (TypeVarTuple, Unpack)

Comments

@jorenham
Copy link
Contributor

jorenham commented May 18, 2025

From the typing spec:

The type tuple[Any, ...] is special in that it is consistent with all tuple types, and assignable to a tuple of any length. This is useful for gradual typing.

And mypy does precisely this:

type Ge0 = tuple[Any, ...]

def ge0_to_0(shape: Ge0) -> tuple[()]:
    return shape  # ✅ true negative
def ge0_to_1(shape: Ge0) -> tuple[int]:
    return shape  # ✅ true negative
def ge0_to_2(shape: Ge0) -> tuple[int, int]:
    return shape  # ✅ true negative

Ok, so far so good.


The typing spec also says the following:

If an unpacked *tuple[Any, ...] is embedded within another tuple, that portion of the tuple is consistent with any tuple of any length.

So tuple[int, *tuple[Any, ...]] is assignable to any tuple with int as first element. But mypy behaves differently:

type Ge1 = tuple[int, *tuple[Any, ...]]

def ge1_to_0(shape: Ge1) -> tuple[()]:
    return shape  # ✅ true positive
def ge1_to_1(shape: Ge1) -> tuple[int]:
    return shape  # ❌ false positive
def ge1_to_2(shape: Ge1) -> tuple[int, int]:
    return shape  # ❌ false positive

full output:

main.py:17: error: Incompatible return value type (got "tuple[int, *tuple[Any, ...]]", expected "tuple[()]")  [return-value]
main.py:19: error: Incompatible return value type (got "tuple[int, *tuple[Any, ...]]", expected "tuple[int]")  [return-value]
main.py:21: error: Incompatible return value type (got "tuple[int, *tuple[Any, ...]]", expected "tuple[int, int]")  [return-value]
Found 3 errors in 1 file (checked 1 source file)

So it's only the last two errors that are false positives.

mypy-play


Some observations:

  • This is the case with (at least) mypy 1.15.0 and on the current master branch.
  • It does not matter if you instead use tuple[Any, *tuple[Any, ...]] and/or use Any in the return types.
  • Pyright behaves correctly here (playground)
@jorenham jorenham added the bug mypy got something wrong label May 18, 2025
@jorenham jorenham changed the title tuple[Any, *tuple[Any, ...]] tuple[Any, *tuple[Any, ...]] is not assignable to other tuple types May 18, 2025
@sterliakov sterliakov added the topic-pep-646 PEP 646 (TypeVarTuple, Unpack) label May 18, 2025
@sterliakov
Copy link
Collaborator

sterliakov commented May 19, 2025

SubtypeVisitor.visit_tuple_type is to blame here. variadic_tuple_subtype declares in its docstring that

the cases where right is fixed or [...] should be handled by the caller.

and yet it's the only check we do there that takes Unpack into account (if false, we continue with checking items count, which is obviously a bad idea when left still has Unpack).

And it's...complicated. Tuples may have fixed prefixes and suffices around unpack, the following is a valid type:

tuple[int, *tuple[int, ...], str]

which means that when RHS has fewer elements, we need to be careful not to count the same RHS item twice.

I have a patch, but I hate it... And doubt it's complete, I'm usually bad at getting index math right from the first try. I'll try to polish it a bit later, feel free to join my efforts!

diff --git a/mypy/subtypes.py b/mypy/subtypes.py
index 84fda7955..d31b7dd9a 100644
--- a/mypy/subtypes.py
+++ b/mypy/subtypes.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from collections.abc import Iterable, Iterator
+from collections.abc import Iterable, Iterator, Sequence
 from contextlib import contextmanager
 from typing import Any, Callable, Final, TypeVar, cast
 from typing_extensions import TypeAlias as _TypeAlias
@@ -782,9 +782,29 @@ class SubtypeVisitor(TypeVisitor[bool]):
             # doesn't have one, we will fall through to False down the line.
             if self.variadic_tuple_subtype(left, right):
                 return True
-            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)):
+            if (li := find_unpack_in_list(left.items)) is not None:
+                if len(left.items) - 1 > len(right.items):
+                    # Even without the unpack we don't have enough items. Sad.
+                    return False
+
+                left_prefix = left.items[:li]
+                left_unp = left.items[li]
+                assert isinstance(left_unp, UnpackType)
+                left_suffix = left.items[li + 1 :]
+
+                right_prefix = right.items[: len(left_prefix)]
+                # No negative indexing - -0 is same as +0.
+                right_suffix = right.items[len(right.items) - len(left_suffix) :]
+                right_middle = right.items[len(left_prefix) : len(right.items) - len(left_suffix)]
+                return (
+                    self._is_subtype_sequence(left_prefix, right_prefix)
+                    and self._is_subtype_sequence(left_suffix, right_suffix)
+                    and self._is_subtype(
+                        left_unp.type, TupleType(right_middle, right.partial_fallback)
+                    )
+                )
+
+            if not self._is_subtype_sequence(left.items, right.items):
                 return False
             if is_named_instance(right.partial_fallback, "builtins.tuple"):
                 # No need to verify fallback. This is useful since the calculated fallback
@@ -801,6 +821,11 @@ class SubtypeVisitor(TypeVisitor[bool]):
         else:
             return False
 
+    def _is_subtype_sequence(self, lefts: Sequence[Type], rights: Sequence[Type]) -> bool:
+        return len(lefts) == len(rights) and all(
+            self._is_subtype(left, right) for left, right in zip(lefts, rights)
+        )
+
     def variadic_tuple_subtype(self, left: TupleType, right: TupleType) -> bool:
         """Check subtyping between two potentially variadic tuples.
 
diff --git a/test-data/unit/check-typevar-tuple.test b/test-data/unit/check-typevar-tuple.test
index d364439f2..0c06f0be2 100644
--- a/test-data/unit/check-typevar-tuple.test
+++ b/test-data/unit/check-typevar-tuple.test
@@ -2628,3 +2628,87 @@ def fn(f: Callable[[*tuple[T]], int]) -> Callable[[*tuple[T]], int]: ...
 def test(*args: Unpack[tuple[T]]) -> int: ...
 reveal_type(fn(test))  # N: Revealed type is "def [T] (T`1) -> builtins.int"
 [builtins fixtures/tuple.pyi]
+
+[case testAnyTuple1]
+from typing import Any
+
+Ge0 = tuple[Any, ...]
+
+def ge0_to_0(shape: Ge0) -> tuple[()]:
+    return shape
+def ge0_to_1(shape: Ge0) -> tuple[int]:
+    return shape
+def ge0_to_2(shape: Ge0) -> tuple[int, int]:
+    return shape
+
+
+Ge1 = tuple[int, *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
+
+Ge2 = tuple[*tuple[Any, ...], int]
+
+def ge2_to_0(shape: Ge2) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[Unpack[Tuple[Any, ...]], int]", expected "Tuple[()]")
+def ge2_to_1(shape: Ge2) -> tuple[int]:
+    return shape
+def ge2_to_2(shape: Ge2) -> tuple[int, int]:
+    return shape
+
+Ge3 = tuple[int, *tuple[Any, ...], str]
+
+def ge3_to_0(shape: Ge3) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[Any, ...]], str]", expected "Tuple[()]")
+def ge3_to_1(shape: Ge3) -> tuple[int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[Any, ...]], str]", expected "Tuple[int]")
+def ge3_to_2(shape: Ge3) -> tuple[int, int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[Any, ...]], str]", expected "Tuple[int, int]")
+def ge3_to_22(shape: Ge3) -> tuple[int, int, str]:
+    return shape
+[builtins fixtures/tuple.pyi]
+
+[case testAnyTuple2]
+Ge0 = tuple[int, ...]
+
+def ge0_to_0(shape: Ge0) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, ...]", expected "Tuple[()]")
+def ge0_to_1(shape: Ge0) -> tuple[int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, ...]", expected "Tuple[int]")
+def ge0_to_2(shape: Ge0) -> tuple[int, int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, ...]", expected "Tuple[int, int]")
+
+
+Ge1 = tuple[int, *tuple[int, ...]]
+
+def ge1_to_0(shape: Ge1) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]]]", expected "Tuple[()]")
+def ge1_to_1(shape: Ge1) -> tuple[int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]]]", expected "Tuple[int]")
+def ge1_to_2(shape: Ge1) -> tuple[int, int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]]]", expected "Tuple[int, int]")
+
+Ge2 = tuple[*tuple[int, ...], int]
+
+def ge2_to_0(shape: Ge2) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[Unpack[Tuple[int, ...]], int]", expected "Tuple[()]")
+def ge2_to_1(shape: Ge2) -> tuple[int]:
+    return shape  # E: Incompatible return value type (got "Tuple[Unpack[Tuple[int, ...]], int]", expected "Tuple[int]")
+def ge2_to_2(shape: Ge2) -> tuple[int, int]:
+    return shape  # E: Incompatible return value type (got "Tuple[Unpack[Tuple[int, ...]], int]", expected "Tuple[int, int]")
+
+Ge3 = tuple[int, *tuple[int, ...], str]
+
+def ge3_to_0(shape: Ge3) -> tuple[()]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]], str]", expected "Tuple[()]")
+def ge3_to_1(shape: Ge3) -> tuple[int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]], str]", expected "Tuple[int]")
+def ge3_to_2(shape: Ge3) -> tuple[int, int]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]], str]", expected "Tuple[int, int]")
+def ge3_to_22(shape: Ge3) -> tuple[int, int, str]:
+    return shape  # E: Incompatible return value type (got "Tuple[int, Unpack[Tuple[int, ...]], str]", expected "Tuple[int, int, str]")
+[builtins fixtures/tuple.pyi]

@jorenham
Copy link
Contributor Author

jorenham commented May 19, 2025

Oof yea that's no simple problem... My knowledge about the mypy codebase is very limited, so I'm not sure how much I could be of help here, given the complexity and all 😅.

It might help reduce the complexity if you exploit the fact that there can only be one nested *tuple. So all tuples will structurally look something like

tuple[{params_before}{nested_unpack}{params_after}]

Then the algo could look roughly like this

  1. check if params_before match from the front (int in your example)
  2. check if params_after match from the back (str in your example)
  3. if both sides match, and the tuple is not exhausted, recurse with nested_unpack (tuple[int, ...] in your example)

This doesn't include the special treatment that tuple[Any, ...] requires, but I'm not sure if that's needed.


For what it's worth, I just posted https://discuss.python.org/t/unbounded-tuple-unions/92472, which might be something to take into consideration here as well, as there might be some overlap.
Although, it might be wise to postpone that, cus that on its own sounds pretty complicated to implement properly 😅


And thanks a lot @sterliakov for looking into this, and so quickly too; I certainly appreciate it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong topic-pep-646 PEP 646 (TypeVarTuple, Unpack)
Projects
None yet
Development

No branches or pull requests

2 participants