Repository: python/mypy
Issue category: Bug Report

Problems with "expected overloaded function" when method hinted via Callback Protocol (⇝`numpy`)
**Bug Report**

`mypy` complains about expecting an overloaded function, when a method is hinted by a Callback-Protocol. This technique is used in the `numpy` stubs, and causes `mypy` to complain when trying to describe numpy scalars with simple protocols.

**To Reproduce**

Below is a MWE [[mypy-playground]](https://mypy-play.net/?mypy=latest&python=3.12&gist=8041e0145f43507bed8c86104a5fb5b2), based on how the [numpy stubs declare the methods on scalars](https://github.com/numpy/numpy/blob/bd1f6067a13f8bbdf99871277426dfaad9f7602d/numpy/__init__.pyi#L3582). Note that [[pyright-playground]](https://pyright-play.net/?code=GYJw9gtgBALgngBwJYDsDmUkQWEMoAK4MYAxmADYA0UAygKYXA1gBu9IFYAhgCYBQ-UhW4BnUVAByAV2xwAYl24wAXPygaoAfS19eOlVABEinjADyBI4OFiJp5ZYAURMCXIUAlGs1QAAmwcSgK%2BvPTA2lqk3BQUOk6ijMxQbgAWHIbASjCeUAC0AHxSsggK2YYAdFXqmgHsnDwhmmEROtGx8YlMLDDpIIYycg45%2BUWDpcOV1UIi4nTSCDh4ogCCvLwuxGSU3jUadUGNe1AtkXqdST19mdm5hXRJUxXHBw18x6c651oJlym9GWKQ1uoweTCeggAHoZaAsljBVusoABeIETbJOTz8IA) does not report any issues.

```python
from typing import Protocol, Self, overload

class NumpyFloat:
    __add__: "FloatOP"

class FloatOP(Protocol):
    @overload
    def __call__(self, other: float) -> NumpyFloat: ...
    @overload
    def __call__(self, other: NumpyFloat) -> NumpyFloat: ...

class SupportsAdd(Protocol):
    @overload
    def __add__(self, other: float) -> Self: ...
    @overload
    def __add__(self, other: NumpyFloat) -> Self: ...

x: SupportsAdd = NumpyFloat()  # expected overloaded function, got "FloatOP"
```

Concrete example with `numpy==2.1.3` (again, `pyright` reports no errors):
 
```python
from typing import Protocol, Self, overload
import numpy as np

class TimeDelta(Protocol):
    def __sub__(self, other: Self, /) -> Self: ...

class TimeStamp[TD: TimeDelta](Protocol):
    @overload
    def __sub__(self, other: Self, /) -> TD: ...
    @overload
    def __sub__(self, other: TD, /) -> Self: ...

x: TimeStamp[np.timedelta64] = np.datetime64("2021-01-01")  # ✅
y: TimeStamp[np.float64] = np.float64(10.0)  # expected overloaded function, got "_FloatOp[_64Bit]"
```


Functions to edit:
mypy/subtypes.py:SubtypeVisitor.visit_instance

Patch:
diff --git a/mypy/subtypes.py b/mypy/subtypes.py
index a63db93fd9cb..11f3421331a5 100644
--- a/mypy/subtypes.py
+++ b/mypy/subtypes.py
@@ -625,8 +625,8 @@ def visit_instance(self, left: Instance) -> bool:
                         return is_named_instance(item, "builtins.object")
         if isinstance(right, LiteralType) and left.last_known_value is not None:
             return self._is_subtype(left.last_known_value, right)
-        if isinstance(right, CallableType):
-            # Special case: Instance can be a subtype of Callable.
+        if isinstance(right, FunctionLike):
+            # Special case: Instance can be a subtype of Callable / Overloaded.
             call = find_member("__call__", left, left, is_operator=True)
             if call:
                 return self._is_subtype(call, right)

Test patch: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
index 5ed2351e33e6..0367be3dde65 100644
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -4215,3 +4215,24 @@ def g4(a: Input[bytes], b: Output[str]) -> None:
     f(a, b)  # E: Cannot infer type argument 1 of "f"
 
 [builtins fixtures/tuple.pyi]
+
+[case testOverloadProtocolSubtyping]
+from typing import Protocol, Self, overload
+
+class NumpyFloat:
+    __add__: "FloatOP"
+
+class FloatOP(Protocol):
+    @overload
+    def __call__(self, other: float) -> NumpyFloat: ...
+    @overload
+    def __call__(self, other: NumpyFloat) -> NumpyFloat: ...
+
+class SupportsAdd(Protocol):
+    @overload
+    def __add__(self, other: float) -> Self: ...
+    @overload
+    def __add__(self, other: NumpyFloat) -> Self: ...
+
+x: SupportsAdd = NumpyFloat()
+[builtins fixtures/tuple.pyi]