From 0ed9283be4f5b8c4190eb3a3779ccf49773e4670 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Sun, 12 Apr 2026 23:49:13 -0700 Subject: [PATCH 1/3] Fix is_overlapping_types for generic callables Fixes #21182 This issue exposed this pre-existing deficiency in is_overlapping_types --- mypy/meet.py | 11 +++++++++++ mypy/test/testtypes.py | 23 ++++++++++++++++++++++- test-data/unit/check-narrowing.test | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/mypy/meet.py b/mypy/meet.py index ee32f239df8c3..a0c54bbe03b32 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -541,6 +541,9 @@ def _type_object_overlap(left: Type, right: Type) -> bool: return False if isinstance(left, CallableType) and isinstance(right, CallableType): + # We run is_callable_compatible in both directions, similar to the logic + # in is_unsafe_overlapping_overload_signatures + # See comments in https://github.com/python/mypy/pull/5476 return is_callable_compatible( left, right, @@ -548,6 +551,14 @@ def _type_object_overlap(left: Type, right: Type) -> bool: is_proper_subtype=False, ignore_pos_arg_names=not overlap_for_overloads, allow_partial_overlap=True, + ) or is_callable_compatible( + right, + left, + is_compat=_is_overlapping_types, + is_proper_subtype=False, + ignore_pos_arg_names=not overlap_for_overloads, + check_args_covariantly=True, + allow_partial_overlap=True, ) call = None diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 6562f541d73bc..662f6b2e7cf00 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -8,7 +8,7 @@ from mypy.erasetype import erase_type, remove_instance_last_known_values from mypy.indirection import TypeIndirectionVisitor from mypy.join import join_types -from mypy.meet import meet_types, narrow_declared_type +from mypy.meet import is_overlapping_types, meet_types, narrow_declared_type from mypy.nodes import ( ARG_NAMED, ARG_OPT, @@ -645,6 +645,27 @@ def assert_simplified_union(self, original: list[Type], union: Type) -> None: assert_equal(make_simplified_union(original), union) assert_equal(make_simplified_union(list(reversed(original))), union) + def test_generic_callable_overlap_is_symmetric(self) -> None: + any_type = AnyType(TypeOfAny.from_omitted_generics) + outer_t = TypeVarType("T", "T", TypeVarId(1), [], self.fx.o, any_type) + outer_s = TypeVarType("S", "S", TypeVarId(2), [], self.fx.o, any_type) + generic_t = TypeVarType("T", "T", TypeVarId(-1), [], self.fx.o, any_type) + + callable_type = CallableType( + [outer_t], [ARG_POS], [None], outer_s, self.fx.function + ) + generic_identity = CallableType( + [generic_t], + [ARG_POS], + [None], + generic_t, + self.fx.function, + variables=[generic_t], + ) + + assert is_overlapping_types(callable_type, generic_identity) + assert is_overlapping_types(generic_identity, callable_type) + # Helpers def tuple(self, *a: Type) -> TupleType: diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 6e3bba6921bff..f8ba0773cf6d3 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3993,3 +3993,21 @@ def f2(func: Callable[..., T], arg: str) -> T: return func(arg) return func(arg) [builtins fixtures/primitives.pyi] + + +[case testNarrowGenericCallableEquality] +# flags: --strict-equality --warn-unreachable +from typing import Callable, TypeVar + +S = TypeVar("S") +T = TypeVar("T") + +def identity(x: T) -> T: + return x + +def msg(cmp_property: Callable[[T], S]) -> None: + if cmp_property == identity: + reveal_type(cmp_property) # N: Revealed type is "def [T] (x: T`-1) -> T`-1" + reveal_type(identity) # N: Revealed type is "def (T`-1) -> S`-2" + return +[builtins fixtures/primitives.pyi] From b0db492ba23008abb36926e73009010d621d05be Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 06:52:08 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/test/testtypes.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 662f6b2e7cf00..ec9af3e669344 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -651,16 +651,9 @@ def test_generic_callable_overlap_is_symmetric(self) -> None: outer_s = TypeVarType("S", "S", TypeVarId(2), [], self.fx.o, any_type) generic_t = TypeVarType("T", "T", TypeVarId(-1), [], self.fx.o, any_type) - callable_type = CallableType( - [outer_t], [ARG_POS], [None], outer_s, self.fx.function - ) + callable_type = CallableType([outer_t], [ARG_POS], [None], outer_s, self.fx.function) generic_identity = CallableType( - [generic_t], - [ARG_POS], - [None], - generic_t, - self.fx.function, - variables=[generic_t], + [generic_t], [ARG_POS], [None], generic_t, self.fx.function, variables=[generic_t] ) assert is_overlapping_types(callable_type, generic_identity) From 3f69a59a39de31660fca78726629e8ad325971a2 Mon Sep 17 00:00:00 2001 From: Shantanu Jain Date: Mon, 13 Apr 2026 11:10:46 -0700 Subject: [PATCH 3/3] add todo --- test-data/unit/check-narrowing.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index f8ba0773cf6d3..581085cf65a65 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -4007,6 +4007,7 @@ def identity(x: T) -> T: def msg(cmp_property: Callable[[T], S]) -> None: if cmp_property == identity: + # TODO: the swapping of these reveal's is not ideal reveal_type(cmp_property) # N: Revealed type is "def [T] (x: T`-1) -> T`-1" reveal_type(identity) # N: Revealed type is "def (T`-1) -> S`-2" return