Repository: python/mypy
Pull request: Fix slicing tuples with nonliterals
This fixes #7056.

The idea is that we don't know what types a tuple slice will result in, other than a tuple of some sort.

For example, `(1,'', 3.3)[:a]` could result in `Tuple[()]`, `Tuple[int]`, `Tuple[int, str]`, or `Tuple[int, str, float]`.

I also fixed a typo I noticed in TypeOfAny.

Problem statement:
Tuple slice by variable results in incorrect type
```python
from typing import Tuple

sz = 2
x: Tuple[int, ...] = (1, 2)[:sz]
```

```console
$ mypy --version
mypy 0.711
$ mypy t.py
mt.py:4: error: Incompatible types in assignment (expression has type "int", variable has type "Tuple[int, ...]")
```

Hints from issue discussion:
this is of course reduced from a larger program to a minimal reproduction
I checked and you can accomplish this with `Literal` and `Final`:
```
from typing import Tuple
from typing_extensions import Final

sz: Final = 2
x: Tuple[int, ...] = (1, 2)[:sz]
```
or if you prefer, a `Literal[2]` annotation should work as well for `sz`.

in reality, sz is not a literal -- it's used in the following way:

```python
ver = tuple(int(p) for p in x.split('.'))
sz = len(ver)
return min_ver[:sz] <= ver <= max_ver[:sz]
```

however this is lost when converting to a minimal testcase

Here's the [current code with the `# type: ignore`](https://github.com/asottile/setup-cfg-fmt/blob/a19c422ae3cb484a3164205709339d7ac0abe1d8/setup_cfg_fmt.py#L251-L257) -- this is definitely a bug and not a question, a slice of a tuple should never return a scalar
Ah, you are correct, sorry I misunderstood the issue. 

E: also it seems the same does not happen for lists, so this is a bug in tuple handling code.
It seems the changes in https://github.com/python/mypy/pull/3514 made the incorrect decision to return the union of elements when given a slice. I'm working on a fix.

Commit message: Fix slicing tuples with nonliterals

Patch to review:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 3c2306de6fdc..67d4a0b0de6b 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -2520,7 +2520,13 @@ def nonliteral_tuple_index_helper(self, left_type: TupleType, index: Expression)
                                       'actual type', 'expected type'):
             return AnyType(TypeOfAny.from_error)
         else:
-            return UnionType.make_simplified_union(left_type.items)
+            if isinstance(index, SliceExpr):
+                # if we index (1,2,3)[:name], we don't know what the return type is,
+                # beyond that it is a tuple of some sort.
+                anys = (AnyType(TypeOfAny.implementation_artifact),)
+                return self.chk.named_generic_type('builtins.tuple', anys)
+            else:
+                return UnionType.make_simplified_union(left_type.items)
 
     def _get_value(self, index: Expression) -> Optional[int]:
         if isinstance(index, IntExpr):
diff --git a/mypy/types.py b/mypy/types.py
index 2f73fb55408a..ca2f4e085bc4 100644
--- a/mypy/types.py
+++ b/mypy/types.py
@@ -84,7 +84,7 @@ class TypeOfAny:
     """
     This class describes different types of Any. Each 'Any' can be of only one type at a time.
     """
-    # Was this Any type was inferred without a type annotation?
+    # Was this Any type inferred without a type annotation?
     unannotated = 1  # type: Final
     # Does this Any come from an explicit type annotation?
     explicit = 2  # type: Final
diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test
index 43ef6d5eaa14..6b76e253f987 100644
--- a/test-data/unit/check-tuples.test
+++ b/test-data/unit/check-tuples.test
@@ -193,7 +193,7 @@ t1[2]     # E: Tuple index out of range
 t1[3]     # E: Tuple index out of range
 t2[1]     # E: Tuple index out of range
 reveal_type(t1[n])     # N: Revealed type is 'Union[__main__.A, __main__.B]'
-reveal_type(t3[n:])    # N: Revealed type is 'Union[__main__.A, __main__.B, __main__.C, __main__.D, __main__.E]'
+reveal_type(t3[n:])    # N: Revealed type is 'builtins.tuple[Any]'
 if int():
     b = t1[(0)] # E: Incompatible types in assignment (expression has type "A", variable has type "B")
 
@@ -1127,7 +1127,7 @@ t[y]  # E: Invalid tuple index type (actual type "str", expected type "Union[int
 t = (0, "")
 x = 0
 y = ""
-reveal_type(t[x:])  # N: Revealed type is 'Union[builtins.int, builtins.str]'
+reveal_type(t[x:])  # N: Revealed type is 'builtins.tuple[Any]'
 t[y:]  # E: Slice index must be an integer or None
 [builtins fixtures/tuple.pyi]
 


Code review comments:
diff_hunk: @@ -2520,7 +2520,13 @@ def nonliteral_tuple_index_helper(self, left_type: TupleType, index: Expression)
                                       'actual type', 'expected type'):
             return AnyType(TypeOfAny.from_error)
         else:
-            return UnionType.make_simplified_union(left_type.items)
+            if isinstance(index, SliceExpr):
+                # if we index (1,2,3)[:name], we don't know what the return type is,
+                # beyond that it is a tuple of some sort.
+                anys = (AnyType(TypeOfAny.implementation_artifact),)
path: mypy/checkexpr.py
text: @author:
I wasn't sure which variant to use here for TypeOfAny, so guidance would be appreciated.

@user1:
hmmm, I was thinking this would end up being `Tuple[Union[the tuple types], ...]` -- that way for homogenous tuples you'd end up with a consistent type

@user1:
or maybe only that specialization for homogenous tuples? 🤔 

@author:
Yeah, I was thinking that too, but then I thought about code like:
```
a = 2
s = (1, 3.3, '')[:a]
s[0] + s[1]
```
Though perhaps I am being too cautious.

@author:
I could also special case homogeneous tuples to return `Tuple[T, ...]`, and for everything else fall back to `Tuple[Any, ...]`. Though for a decision like this I will defer to @user2 or someone else who wants to chime in here.

@user1:
that sounds fine to me 👍