Repository: python/mypy
Issue category: Bug Report

Issue:
Regression: IO[str] not compatible with SupportsWrite[AnyStr]
This code generates a false positive, but type checked cleanly on mypy 1.11:
```py
from typing import IO
from shutil import copyfileobj

f: IO[str]
copyfileobj(f, f)  # Cannot infer type argument 1 of "copyfileobj"
```
I believe the reason is that the definition of `IO.write` changed in typeshed, and this triggered a mypy bug. Here's how `IO.write` is currently defined:
```py
class IO(Iterator[AnyStr]):
    ...
    @abstractmethod
    @overload
    def write(self: IO[bytes], s: ReadableBuffer, /) -> int: ...
    @abstractmethod
    @overload
    def write(self, s: AnyStr, /) -> int: ...
```


Hints:

Patch:
diff --git a/mypy/typeops.py b/mypy/typeops.py
index 7f530d13d4e2..0699cda53cfa 100644
--- a/mypy/typeops.py
+++ b/mypy/typeops.py
@@ -14,6 +14,7 @@
 from mypy.expandtype import expand_type, expand_type_by_instance
 from mypy.maptype import map_instance_to_supertype
 from mypy.nodes import (
+    ARG_OPT,
     ARG_POS,
     ARG_STAR,
     ARG_STAR2,
@@ -305,9 +306,27 @@ class B(A): pass
 
     """
     if isinstance(method, Overloaded):
-        items = [
-            bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items
-        ]
+        items = []
+        original_type = get_proper_type(original_type)
+        for c in method.items:
+            if isinstance(original_type, Instance):
+                # Filter based on whether declared self type can match actual object type.
+                # For example, if self has type C[int] and method is accessed on a C[str] value,
+                # omit this item. This is best effort since bind_self can be called in many
+                # contexts, and doing complete validation might trigger infinite recursion.
+                #
+                # Note that overload item filtering normally happens elsewhere. This is needed
+                # at least during constraint inference.
+                keep = is_valid_self_type_best_effort(c, original_type)
+            else:
+                keep = True
+            if keep:
+                items.append(bind_self(c, original_type, is_classmethod, ignore_instances))
+        if len(items) == 0:
+            # If no item matches, returning all items helps avoid some spurious errors
+            items = [
+                bind_self(c, original_type, is_classmethod, ignore_instances) for c in method.items
+            ]
         return cast(F, Overloaded(items))
     assert isinstance(method, CallableType)
     func = method
@@ -379,6 +398,43 @@ class B(A): pass
     return cast(F, res)
 
 
+def is_valid_self_type_best_effort(c: CallableType, self_type: Instance) -> bool:
+    """Quickly check if self_type might match the self in a callable.
+
+    Avoid performing any complex type operations. This is performance-critical.
+
+    Default to returning True if we don't know (or it would be too expensive).
+    """
+    if (
+        self_type.args
+        and c.arg_types
+        and isinstance((arg_type := get_proper_type(c.arg_types[0])), Instance)
+        and c.arg_kinds[0] in (ARG_POS, ARG_OPT)
+        and arg_type.args
+        and self_type.type.fullname != "functools._SingleDispatchCallable"
+    ):
+        if self_type.type is not arg_type.type:
+            # We can't map to supertype, since it could trigger expensive checks for
+            # protocol types, so we consevatively assume this is fine.
+            return True
+
+        # Fast path: no explicit annotation on self
+        if all(
+            (
+                type(arg) is TypeVarType
+                and type(arg.upper_bound) is Instance
+                and arg.upper_bound.type.fullname == "builtins.object"
+            )
+            for arg in arg_type.args
+        ):
+            return True
+
+        from mypy.meet import is_overlapping_types
+
+        return is_overlapping_types(self_type, c.arg_types[0])
+    return True
+
+
 def erase_to_bound(t: Type) -> Type:
     # TODO: use value restrictions to produce a union?
     t = get_proper_type(t)


Test patch:
diff --git a/test-data/unit/check-overloading.test b/test-data/unit/check-overloading.test
index 48d5996b226f..e414c1c9b0b6 100644
--- a/test-data/unit/check-overloading.test
+++ b/test-data/unit/check-overloading.test
@@ -6750,3 +6750,21 @@ def foo(x: object) -> str: ...
 def bar(x: int) -> int: ...
 @overload
 def bar(x: Any) -> str: ...
+
+[case testOverloadOnInvalidTypeArgument]
+from typing import TypeVar, Self, Generic, overload
+
+class C: pass
+
+T = TypeVar("T", bound=C)
+
+class D(Generic[T]):
+    @overload
+    def f(self, x: int) -> int: ...
+    @overload
+    def f(self, x: str) -> str: ...
+    def f(Self, x): ...
+
+a: D[str]  # E: Type argument "str" of "D" must be a subtype of "C"
+reveal_type(a.f(1))  # N: Revealed type is "builtins.int"
+reveal_type(a.f("x"))  # N: Revealed type is "builtins.str"
diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test
index ee7556461fd3..5ed2351e33e6 100644
--- a/test-data/unit/check-protocols.test
+++ b/test-data/unit/check-protocols.test
@@ -4127,3 +4127,91 @@ class P(Protocol):
 
 class C(P): ...
 C(0)  # OK
+
+[case testTypeVarValueConstraintAgainstGenericProtocol]
+from typing import TypeVar, Generic, Protocol, overload
+
+T_contra = TypeVar("T_contra", contravariant=True)
+AnyStr = TypeVar("AnyStr", str, bytes)
+
+class SupportsWrite(Protocol[T_contra]):
+    def write(self, s: T_contra, /) -> None: ...
+
+class Buffer: ...
+
+class IO(Generic[AnyStr]):
+    @overload
+    def write(self: IO[bytes], s: Buffer, /) -> None: ...
+    @overload
+    def write(self, s: AnyStr, /) -> None: ...
+    def write(self, s): ...
+
+def foo(fdst: SupportsWrite[AnyStr]) -> None: ...
+
+x: IO[str]
+foo(x)
+
+[case testTypeVarValueConstraintAgainstGenericProtocol2]
+from typing import Generic, Protocol, TypeVar, overload
+
+AnyStr = TypeVar("AnyStr", str, bytes)
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True)
+
+class SupportsRead(Generic[T_co]):
+    def read(self) -> T_co: ...
+
+class SupportsWrite(Protocol[T_contra]):
+    def write(self, s: T_contra) -> object: ...
+
+def copyfileobj(fsrc: SupportsRead[AnyStr], fdst: SupportsWrite[AnyStr]) -> None: ...
+
+class WriteToMe(Generic[AnyStr]):
+    @overload
+    def write(self: WriteToMe[str], s: str) -> int: ...
+    @overload
+    def write(self: WriteToMe[bytes], s: bytes) -> int: ...
+    def write(self, s): ...
+
+class WriteToMeOrReadFromMe(WriteToMe[AnyStr], SupportsRead[AnyStr]): ...
+
+copyfileobj(WriteToMeOrReadFromMe[bytes](), WriteToMe[bytes]())
+
+[case testOverloadedMethodWithExplictSelfTypes]
+from typing import Generic, overload, Protocol, TypeVar, Union
+
+AnyStr = TypeVar("AnyStr", str, bytes)
+T_co = TypeVar("T_co", covariant=True)
+T_contra = TypeVar("T_contra", contravariant=True)
+
+class SupportsRead(Protocol[T_co]):
+    def read(self) -> T_co: ...
+
+class SupportsWrite(Protocol[T_contra]):
+    def write(self, s: T_contra) -> int: ...
+
+class Input(Generic[AnyStr]):
+    def read(self) -> AnyStr: ...
+
+class Output(Generic[AnyStr]):
+    @overload
+    def write(self: Output[str], s: str) -> int: ...
+    @overload
+    def write(self: Output[bytes], s: bytes) -> int: ...
+    def write(self, s: Union[str, bytes]) -> int: ...
+
+def f(src: SupportsRead[AnyStr], dst: SupportsWrite[AnyStr]) -> None: ...
+
+def g1(a: Input[bytes], b: Output[bytes]) -> None:
+    f(a, b)
+
+def g2(a: Input[bytes], b: Output[bytes]) -> None:
+    f(a, b)
+
+def g3(a: Input[str], b: Output[bytes]) -> None:
+    f(a, b)  # E: Cannot infer type argument 1 of "f"
+
+def g4(a: Input[bytes], b: Output[str]) -> None:
+    f(a, b)  # E: Cannot infer type argument 1 of "f"
+
+[builtins fixtures/tuple.pyi]