From f0c1fdcf1a2cd3fb8b1c318663091600e7f63529 Mon Sep 17 00:00:00 2001 From: bahtya Date: Thu, 9 Apr 2026 02:56:45 +0800 Subject: [PATCH 1/2] fix: prevent false unreachable error when comparing generic callables When comparing a generic Callable parameter with a generic function using ==, mypy incorrectly concluded the types could never overlap and marked the if-body as unreachable. Fix by extending shallow_erase_type_for_equality to handle CallableType and using erased current_type in the equality overlap checks. Fixes #21182 Signed-off-by: bahtya --- mypy/checker.py | 7 +++++-- mypy/erasetype.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8775f1ddef294..5ec8f126b643b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -8478,13 +8478,16 @@ def conditional_types( if from_equality: # We erase generic args because values with different generic types can compare equal # For instance, cast(list[str], []) and cast(list[int], []) + # We also erase the current type for the overlap check, to correctly handle + # generic callables with different type variables (see mypy#21182). + erased_current = shallow_erase_type_for_equality(current_type) proposed_type = shallow_erase_type_for_equality(proposed_type) - if not is_overlapping_types(current_type, proposed_type, ignore_promotions=False): + if not is_overlapping_types(erased_current, proposed_type, ignore_promotions=False): # Equality narrowing is one of the places at runtime where subtyping with promotion # does happen to match runtime semantics # Expression is never of any type in proposed_type_ranges return UninhabitedType(), default - if not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): + if not is_overlapping_types(erased_current, proposed_type, ignore_promotions=True): return default, default else: if not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): diff --git a/mypy/erasetype.py b/mypy/erasetype.py index cb8d66f292dd3..44942484b2920 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -288,7 +288,13 @@ def visit_union_type(self, t: UnionType) -> Type: def shallow_erase_type_for_equality(typ: Type) -> ProperType: - """Erase type variables from Instance's""" + """Erase type variables from types for equality narrowing. + + At runtime, generic type parameters are erased, so values with different + generic types can compare equal (e.g. ``cast(list[str], []) == cast(list[int], [])``). + This function erases generic type information so that equality-based type + narrowing does not incorrectly conclude that two values can never be equal. + """ p_typ = get_proper_type(typ) if isinstance(p_typ, Instance): if not p_typ.args: @@ -298,4 +304,18 @@ def shallow_erase_type_for_equality(typ: Type) -> ProperType: if isinstance(p_typ, UnionType): items = [shallow_erase_type_for_equality(item) for item in p_typ.items] return UnionType.make_union(items) + if isinstance(p_typ, CallableType): + # Erase generic type variables from non-type-object callables. + # Type objects (classes like TupleLike) keep their type identity, + # but regular generic functions have their type vars erased to Any + # since at runtime, generic functions with different type args can + # be the same object (e.g. the identity function). + if not p_typ.variables or p_typ.is_type_obj(): + return p_typ + any_type = AnyType(TypeOfAny.special_form) + return p_typ.copy_modified( + arg_types=[any_type for _ in p_typ.arg_types], + ret_type=any_type, + variables=(), + ) return p_typ From 07b6987bd46984ee3918a0505dbd778e75eeae7a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:58:51 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/erasetype.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/erasetype.py b/mypy/erasetype.py index 44942484b2920..51981e71d7877 100644 --- a/mypy/erasetype.py +++ b/mypy/erasetype.py @@ -314,8 +314,6 @@ def shallow_erase_type_for_equality(typ: Type) -> ProperType: return p_typ any_type = AnyType(TypeOfAny.special_form) return p_typ.copy_modified( - arg_types=[any_type for _ in p_typ.arg_types], - ret_type=any_type, - variables=(), + arg_types=[any_type for _ in p_typ.arg_types], ret_type=any_type, variables=() ) return p_typ