Skip to content

Commit f0c1fdc

Browse files
committed
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 <bahtyar153@qq.com>
1 parent bb05513 commit f0c1fdc

File tree

2 files changed

+26
-3
lines changed

2 files changed

+26
-3
lines changed

mypy/checker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8478,13 +8478,16 @@ def conditional_types(
84788478
if from_equality:
84798479
# We erase generic args because values with different generic types can compare equal
84808480
# For instance, cast(list[str], []) and cast(list[int], [])
8481+
# We also erase the current type for the overlap check, to correctly handle
8482+
# generic callables with different type variables (see mypy#21182).
8483+
erased_current = shallow_erase_type_for_equality(current_type)
84818484
proposed_type = shallow_erase_type_for_equality(proposed_type)
8482-
if not is_overlapping_types(current_type, proposed_type, ignore_promotions=False):
8485+
if not is_overlapping_types(erased_current, proposed_type, ignore_promotions=False):
84838486
# Equality narrowing is one of the places at runtime where subtyping with promotion
84848487
# does happen to match runtime semantics
84858488
# Expression is never of any type in proposed_type_ranges
84868489
return UninhabitedType(), default
8487-
if not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):
8490+
if not is_overlapping_types(erased_current, proposed_type, ignore_promotions=True):
84888491
return default, default
84898492
else:
84908493
if not is_overlapping_types(current_type, proposed_type, ignore_promotions=True):

mypy/erasetype.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,13 @@ def visit_union_type(self, t: UnionType) -> Type:
288288

289289

290290
def shallow_erase_type_for_equality(typ: Type) -> ProperType:
291-
"""Erase type variables from Instance's"""
291+
"""Erase type variables from types for equality narrowing.
292+
293+
At runtime, generic type parameters are erased, so values with different
294+
generic types can compare equal (e.g. ``cast(list[str], []) == cast(list[int], [])``).
295+
This function erases generic type information so that equality-based type
296+
narrowing does not incorrectly conclude that two values can never be equal.
297+
"""
292298
p_typ = get_proper_type(typ)
293299
if isinstance(p_typ, Instance):
294300
if not p_typ.args:
@@ -298,4 +304,18 @@ def shallow_erase_type_for_equality(typ: Type) -> ProperType:
298304
if isinstance(p_typ, UnionType):
299305
items = [shallow_erase_type_for_equality(item) for item in p_typ.items]
300306
return UnionType.make_union(items)
307+
if isinstance(p_typ, CallableType):
308+
# Erase generic type variables from non-type-object callables.
309+
# Type objects (classes like TupleLike) keep their type identity,
310+
# but regular generic functions have their type vars erased to Any
311+
# since at runtime, generic functions with different type args can
312+
# be the same object (e.g. the identity function).
313+
if not p_typ.variables or p_typ.is_type_obj():
314+
return p_typ
315+
any_type = AnyType(TypeOfAny.special_form)
316+
return p_typ.copy_modified(
317+
arg_types=[any_type for _ in p_typ.arg_types],
318+
ret_type=any_type,
319+
variables=(),
320+
)
301321
return p_typ

0 commit comments

Comments
 (0)