From 85f084fae1d4e3f2d7b271f8d7f69189d3739755 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 12 Mar 2026 18:33:51 -0700 Subject: [PATCH 1/4] Be strict about arg names in protocol compatibility --- mypy/subtypes.py | 5 +-- test-data/unit/check-protocols.test | 65 +++++++++++++++++++++-------- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 66d7a95eb425..6f7e2513f9dc 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1218,7 +1218,6 @@ def f(self) -> A: ... for member in right.type.protocol_members: if member in members_not_to_check: continue - ignore_names = member != "__call__" # __call__ can be passed kwargs # The third argument below indicates to what self type is bound. # We always bind self to the subtype. (Similarly to nominal types). supertype = find_member(member, right, left) @@ -1234,9 +1233,7 @@ def f(self) -> A: ... # Nominal check currently ignores arg names # NOTE: If we ever change this, be sure to also change the call to # SubtypeVisitor.build_subtype_kind(...) down below. - is_compat = is_subtype( - subtype, supertype, ignore_pos_arg_names=ignore_names, options=options - ) + is_compat = is_subtype(subtype, supertype, options=options) else: is_compat = is_proper_subtype(subtype, supertype) if not is_compat: diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index a51445ef0b5b..39b7819b093a 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -3391,7 +3391,7 @@ test(D) # OK from typing import Any, Protocol class P(Protocol): - def foo(self, obj: Any) -> int: ... + def foo(self, obj: Any, /) -> int: ... class B: def foo(self) -> int: ... @@ -3403,7 +3403,7 @@ test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def foo(obj: Any) -> int \ + # N: def foo(Any, /) -> int \ # N: Got: \ # N: def foo(self: C) -> str @@ -3411,7 +3411,7 @@ test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" from typing import Any, Protocol class P(Protocol): - def foo(self, obj: B) -> int: ... + def foo(self, obj: B, /) -> int: ... class B: def foo(self) -> int: ... @@ -3423,7 +3423,7 @@ test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def foo(obj: B) -> int \ + # N: def foo(B, /) -> int \ # N: Got: \ # N: def foo(self: C) -> int @@ -3432,9 +3432,9 @@ from typing import Any, Protocol, overload class P(Protocol): @overload - def foo(self, obj: Any, arg: int) -> int: ... + def foo(self, obj: Any, /, arg: int) -> int: ... @overload - def foo(self, obj: Any, arg: str) -> str: ... + def foo(self, obj: Any, /, arg: str) -> str: ... class B: @overload @@ -3458,9 +3458,9 @@ test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ # N: @overload \ - # N: def foo(obj: Any, arg: int) -> int \ + # N: def foo(Any, /, arg: int) -> int \ # N: @overload \ - # N: def foo(obj: Any, arg: str) -> str \ + # N: def foo(Any, /, arg: str) -> str \ # N: Got: \ # N: @overload \ # N: def foo(self: C, arg: int) -> int \ @@ -3517,7 +3517,7 @@ test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" from typing import Any, Protocol, Generic, List, TypeVar class P(Protocol): - def foo(self, obj: Any) -> List[int]: ... + def foo(self, obj: Any, /) -> List[int]: ... T = TypeVar("T") class A(Generic[T]): @@ -3532,7 +3532,7 @@ test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def foo(obj: Any) -> list[int] \ + # N: def foo(Any, /) -> list[int] \ # N: Got: \ # N: def foo(self: A[list[str]]) -> list[str] [builtins fixtures/list.pyi] @@ -3567,7 +3567,7 @@ from typing import Protocol, TypeVar, Union T = TypeVar("T") class P(Protocol): - def foo(self, arg: T) -> T: ... + def foo(self, arg: T, /) -> T: ... class B: def foo(self: T) -> T: ... @@ -3579,7 +3579,7 @@ test(B) # OK test(C) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def [T] foo(arg: T) -> T \ + # N: def [T] foo(T, /) -> T \ # N: Got: \ # N: def [T] foo(self: T) -> T | int @@ -3711,7 +3711,7 @@ test(d) # E: Argument 1 to "test" has incompatible type "type[D]"; expected "P" from typing import Any, Protocol, Type class P(Protocol): - def foo(self, cls: Any) -> int: ... + def foo(self, cls: Any, /) -> int: ... class B: def foo(self) -> int: ... @@ -3725,7 +3725,7 @@ test(b) # OK test(c) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def foo(cls: Any) -> int \ + # N: def foo(Any, /) -> int \ # N: Got: \ # N: def foo(self: C) -> str @@ -3759,7 +3759,7 @@ from typing import Protocol, Type, TypeVar, Union T = TypeVar("T") class P(Protocol): - def foo(self, arg: T) -> T: ... + def foo(self, arg: T, /) -> T: ... class B: def foo(self: T) -> T: ... @@ -3773,7 +3773,7 @@ test(b) # OK test(c) # E: Argument 1 to "test" has incompatible type "type[C]"; expected "P" \ # N: Following member(s) of "C" have conflicts: \ # N: Expected: \ - # N: def [T] foo(arg: T) -> T \ + # N: def [T] foo(T, /) -> T \ # N: Got: \ # N: def [T] foo(self: T) -> T | int @@ -3782,7 +3782,7 @@ from typing import Any, Protocol, TypeVar T = TypeVar("T", contravariant=True) class P(Protocol[T]): - def foo(self, obj: T) -> int: ... + def foo(self, obj: T, /) -> int: ... class B: def foo(self) -> int: ... @@ -3804,7 +3804,8 @@ class B: S = TypeVar("S") def test(arg: P[S]) -> S: ... b: Type[B] -reveal_type(test(b)) # N: Revealed type is "__main__.B" +reveal_type(test(b)) # N: Revealed type is "__main__.B" \ + # E: Argument 1 to "test" has incompatible type "type[B]"; expected "P[B]" [case testTypeAliasInProtocolBody] from typing import Protocol, List @@ -4746,3 +4747,31 @@ tmp/a.py:8: note: Expected: tmp/a.py:8: note: def f(self) -> PNested tmp/a.py:8: note: Got: tmp/a.py:8: note: def f(self) -> CNested + +[case testProtocolArgNames] +from typing import Protocol + +class P1(Protocol): + def foo(self, a: int) -> None: ... + +class C1: + def foo(self, b: int) -> None: pass + +x1: P1 = C1() # E: Incompatible types in assignment (expression has type "C1", variable has type "P1") + +class P2(Protocol): + def foo(self, a: int) -> None: ... + +class C2: + def foo(self, *args: int) -> None: pass + +x2: P2 = C2() # E: Incompatible types in assignment (expression has type "C2", variable has type "P2") + +class P3(Protocol): + def foo(self, a: int, /) -> None: ... + +class C3: + def foo(self, *args: int) -> None: pass + +okay3: P3 = C3() +[builtins fixtures/tuple.pyi] From 666fab251fdb61003ed21677e7a9fc1d6d7c2489 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 12 Mar 2026 19:17:51 -0700 Subject: [PATCH 2/4] review --- mypy/subtypes.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 6f7e2513f9dc..9bf284e1e2b3 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1277,14 +1277,8 @@ def f(self) -> A: ... if IS_CLASS_OR_STATIC in superflags and IS_CLASS_OR_STATIC not in subflags: return False - if not proper_subtype: - # Nominal check currently ignores arg names, but __call__ is special for protocols - ignore_names = right.type.protocol_members != ["__call__"] - else: - ignore_names = False subtype_kind = SubtypeVisitor.build_subtype_kind( - subtype_context=SubtypeContext(ignore_pos_arg_names=ignore_names), - proper_subtype=proper_subtype, + subtype_context=SubtypeContext(), proper_subtype=proper_subtype ) type_state.record_subtype_cache_entry(subtype_kind, left, right) return True From d60f5ce31cd2a5dddcf5db45ff99bfde5d650fd4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 13 Mar 2026 16:19:47 -0700 Subject: [PATCH 3/4] show conflict --- mypy/messages.py | 2 +- test-data/unit/check-protocols.test | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 51bb0b7ee9be..4bde8e4686b8 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -3113,7 +3113,7 @@ def get_conflict_protocol_types( subtype = mypy.typeops.get_protocol_member(left, member, class_obj) if not subtype: continue - is_compat = is_subtype(subtype, supertype, ignore_pos_arg_names=True, options=options) + is_compat = is_subtype(subtype, supertype, options=options) if not is_compat: conflicts.append((member, subtype, supertype, False)) superflags = get_member_flags(member, right) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 39b7819b093a..c901b1fd102c 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2642,6 +2642,13 @@ func(a) func(b) # E: Argument 1 to "func" has incompatible type "Bad"; expected "One" \ # N: "One.__call__" has type "def __call__(self, x: str) -> None" [out] +main:19: error: Argument 1 to "func" has incompatible type "Bad"; expected "One" +main:19: note: Following member(s) of "Bad" have conflicts: +main:19: note: Expected: +main:19: note: def __call__(self, x: str) -> None +main:19: note: Got: +main:19: note: def __call__(self, zzz: str) -> None +main:19: note: "One.__call__" has type "def __call__(self, x: str) -> None" [case testJoinProtocolCallback] from typing import Protocol, Callable @@ -3796,7 +3803,7 @@ from typing import Any, Protocol, TypeVar, Type T = TypeVar("T", contravariant=True) class P(Protocol[T]): - def foo(self, obj: T) -> int: ... + def foo(self, obj: T, /) -> int: ... class B: def foo(self) -> int: ... @@ -3804,8 +3811,8 @@ class B: S = TypeVar("S") def test(arg: P[S]) -> S: ... b: Type[B] -reveal_type(test(b)) # N: Revealed type is "__main__.B" \ - # E: Argument 1 to "test" has incompatible type "type[B]"; expected "P[B]" +reveal_type(test(b)) # N: Revealed type is "__main__.B" + [case testTypeAliasInProtocolBody] from typing import Protocol, List @@ -4757,7 +4764,12 @@ class P1(Protocol): class C1: def foo(self, b: int) -> None: pass -x1: P1 = C1() # E: Incompatible types in assignment (expression has type "C1", variable has type "P1") +x1: P1 = C1() # E: Incompatible types in assignment (expression has type "C1", variable has type "P1") \ + # N: Following member(s) of "C1" have conflicts: \ + # N: Expected: \ + # N: def foo(self, a: int) -> None \ + # N: Got: \ + # N: def foo(self, b: int) -> None class P2(Protocol): def foo(self, a: int) -> None: ... @@ -4765,7 +4777,12 @@ class P2(Protocol): class C2: def foo(self, *args: int) -> None: pass -x2: P2 = C2() # E: Incompatible types in assignment (expression has type "C2", variable has type "P2") +x2: P2 = C2() # E: Incompatible types in assignment (expression has type "C2", variable has type "P2") \ + # N: Following member(s) of "C2" have conflicts: \ + # N: Expected: \ + # N: def foo(self, a: int) -> None \ + # N: Got: \ + # N: def foo(self, *args: int) -> None class P3(Protocol): def foo(self, a: int, /) -> None: ... From 1894005189e231890cb5f3af0aa9f6168624fa11 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 13 Mar 2026 16:33:13 -0700 Subject: [PATCH 4/4] . --- test-data/unit/check-protocols.test | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index c901b1fd102c..21548f6b2f8b 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2640,15 +2640,13 @@ b: Bad func(a) func(b) # E: Argument 1 to "func" has incompatible type "Bad"; expected "One" \ + # N: Following member(s) of "Bad" have conflicts: \ + # N: Expected: \ + # N: def __call__(self, x: str) -> None \ + # N: Got: \ + # N: def __call__(self, zzz: str) -> None \ # N: "One.__call__" has type "def __call__(self, x: str) -> None" -[out] -main:19: error: Argument 1 to "func" has incompatible type "Bad"; expected "One" -main:19: note: Following member(s) of "Bad" have conflicts: -main:19: note: Expected: -main:19: note: def __call__(self, x: str) -> None -main:19: note: Got: -main:19: note: def __call__(self, zzz: str) -> None -main:19: note: "One.__call__" has type "def __call__(self, x: str) -> None" + [case testJoinProtocolCallback] from typing import Protocol, Callable