Repository: python/mypy
Issue category: Bug Report

Issue:
Change in inference of overloads involving `Hashable` and `slice` now that `slice` is generic
**Bug Report**

Consider the following Python snippet, saved as `foo.pyi`:

```py
from typing import assert_type, overload, Hashable

class Foo: ...

class DataFrame:
    @overload
    def __getitem__(self, key: slice) -> DataFrame: ...
    @overload
    def __getitem__(self, key: Hashable) -> Foo: ...


df = DataFrame()
assert_type(df[1:], DataFrame)
assert_type(df[:2], DataFrame)
```

Prior to [the recent change to make `slice` generic in typeshed](https://github.com/python/typeshed/pull/11637), mypy used to emit no errors on this snippet. However, mypy on the `master` branch emits the following errors:

```
foo.pyi:7: error: Overloaded function signatures 1 and 2 overlap with incompatible return types  [overload-overlap]
foo.pyi:13: error: Expression is of type "Any", not "DataFrame"  [assert-type]
foo.pyi:14: error: Expression is of type "Any", not "DataFrame"  [assert-type]
```

The first error here seems reasonable to me, but it's unclear to me why mypy now infers `Any` as the result of the `df` subscriptions. This snippet is minimized from the sole new mypy_primer error reported in https://github.com/python/typeshed/pull/11637#issuecomment-2435268577.

**To Reproduce**

https://mypy-play.net/?mypy=master&python=3.12&gist=339ba7f72c9048770ce15d6dc75207a1

**Your Environment**

- Mypy version used: mypy 1.14.0+dev.2ebc690279c7e20ed8bec006787030c5ba57c40e (compiled: no)



Hints:
I think this is because since python 3.12, slice is hashable, hence it satisfies both overloads and mypy performs a join of the return types. When you run with 3.11 the error disappears:

https://mypy-play.net/?mypy=master&python=3.11&gist=339ba7f72c9048770ce15d6dc75207a1
@AlexWaygood Uhh, I just discovered that it actually seems to be related to the bug #2410 we just encountered in typeshed as well, because when you use non-literal slices it also disappears!

```python
from typing import assert_type, overload, Hashable

class Foo: ...

class DataFrame:
    @overload
    def __getitem__(self, key: slice, /) -> "DataFrame": ...
    @overload
    def __getitem__(self, key: Hashable, /) -> Foo: ...


df = DataFrame()
assert_type(df[1:], DataFrame)  # ❌
assert_type(df[:2], DataFrame)  # ❌
assert_type(df[slice(1, None)], DataFrame)  # ✅
assert_type(df[slice(None, 2)], DataFrame)  # ✅
```

https://mypy-play.net/?mypy=master&python=3.12&gist=935d2927539dbe01d6727037ace98469


----

I was wondering about this because according to https://mypy.readthedocs.io/en/stable/more_types.html#type-checking-calls-to-overloads, no join operation should happen. I guess there is some legacy code responsible for dealing with literal slices in `__getitem__` that's going haywire?

> I guess there is some legacy code responsible for dealing with literal slices in `__getitem__` that's going haywire?

~Indeed, this is actually hardcoded into the expression checker:~ Nevermind, I had my wires crossed about what this issue was about, sorry for the noise!

[https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L5609-L5619.](https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L5609-L5619)
Actually, my previous comment wasn't as far from the mark as I thought :laughing: 

The difference between slice expressions and non-literal slices does come down to the hardcoded analysis in the expression checker, specifically the return:

https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L5619

This erases any type information and makes all slice expressions be treated as `slice[Any, Any, Any]`. Manually passing a slice of type `slice[Any, Any, Any]` indeed has the same behavior:
```python
df = DataFrame()
x: slice[Any, Any, Any]
assert_type(df[x], DataFrame)  # E: Expression is of type "Any", not "DataFrame"
```
I'm not fully sure why passing `slice[Any, Any, Any]` causes the overload return type to resolve to `Any`, but I think it may have to do with the way [`infer_overload_return_type`](https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L2838) resolves ambiguous overloads, specifically now that this check will resolve to True:
https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L2862
so this branch can no longer be taken:
https://github.com/python/mypy/blob/2ebc690279c7e20ed8bec006787030c5ba57c40e/mypy/checkexpr.py#L2882-L2884

I think the fix would be to just properly infer the generic type arguments for slice expressions. Naively that could look something like the following, though we'd probably want to make it match the behavior of `slice.__new__` in typeshed.
```diff
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -5612,11 +5612,15 @@ class ExpressionChecker(ExpressionVisitor[Type]):
         except KeyError:
             supports_index = self.chk.named_type("builtins.int")  # thanks, fixture life
         expected = make_optional_type(supports_index)
+        type_args = []
         for index in [e.begin_index, e.end_index, e.stride]:
             if index:
                 t = self.accept(index)
                 self.chk.check_subtype(t, expected, index, message_registry.INVALID_SLICE_INDEX)
-        return self.named_type("builtins.slice")
+                type_args.append(t)
+            else:
+                type_args.append(NoneType())
+        return self.chk.named_generic_type("builtins.slice", type_args)
```

Patch:
diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py
index 577576a4e5f8..11a9cffe18c3 100644
--- a/mypy/checkexpr.py
+++ b/mypy/checkexpr.py
@@ -5612,11 +5612,15 @@ def visit_slice_expr(self, e: SliceExpr) -> Type:
         except KeyError:
             supports_index = self.chk.named_type("builtins.int")  # thanks, fixture life
         expected = make_optional_type(supports_index)
+        type_args = []
         for index in [e.begin_index, e.end_index, e.stride]:
             if index:
                 t = self.accept(index)
                 self.chk.check_subtype(t, expected, index, message_registry.INVALID_SLICE_INDEX)
-        return self.named_type("builtins.slice")
+                type_args.append(t)
+            else:
+                type_args.append(NoneType())
+        return self.chk.named_generic_type("builtins.slice", type_args)
 
     def visit_list_comprehension(self, e: ListComprehension) -> Type:
         return self.check_generator_or_comprehension(


Test patch:
diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test
index d5ddc910bcd6..cd26c9bb408a 100644
--- a/test-data/unit/check-expressions.test
+++ b/test-data/unit/check-expressions.test
@@ -1178,8 +1178,8 @@ class B: pass
 [case testSlicingWithInvalidBase]
 
 a: A
-a[1:2] # E: Invalid index type "slice" for "A"; expected type "int"
-a[:]   # E: Invalid index type "slice" for "A"; expected type "int"
+a[1:2] # E: Invalid index type "slice[int, int, None]" for "A"; expected type "int"
+a[:]   # E: Invalid index type "slice[None, None, None]" for "A"; expected type "int"
 class A:
   def __getitem__(self, n: int) -> 'A': pass
 [builtins fixtures/slice.pyi]