diff --git a/mypy/checker.py b/mypy/checker.py index 8775f1ddef29..5ec8f126b643 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 cb8d66f292dd..51981e71d787 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,16 @@ 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