From b8488e153ecf96dc2fdf7b8b317aa9cf1c1aeb7f Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 6 Feb 2026 00:05:47 -0800 Subject: [PATCH] Unsoundly narrow away from None with custom eq In #20754 we see some primer hits where this unsoundness is beneficial --- mypy/checker.py | 9 ++++++++- mypy/types_utils.py | 3 +++ test-data/unit/check-narrowing.test | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index cb5cac93e8109..ae515601ebd1a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6746,7 +6746,14 @@ def narrow_type_by_identity_equality( if i == j: continue # If we compare to a target with custom __eq__, we cannot narrow at all - or_if_maps.append({}) + if is_overlapping_none(expr_type) and not is_overlapping_none( + operand_types[j] + ): + # Narrow away from None. This is unsound, we're hoping that no one + # has a custom __eq__ that returns True for None. + or_if_maps.append({operands[i]: remove_optional(expr_type)}) + else: + or_if_maps.append({operands[i]: expr_type}) continue target_type = operand_types[j] if should_coerce_literals: diff --git a/mypy/types_utils.py b/mypy/types_utils.py index 3c1dcb427f29a..160f6c0365d63 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -26,6 +26,7 @@ TypeAliasType, TypeType, TypeVarType, + UninhabitedType, UnionType, UnpackType, flatten_nested_unions, @@ -134,6 +135,8 @@ def remove_optional(typ: Type) -> Type: return UnionType.make_union( [t for t in typ.items if not isinstance(get_proper_type(t), NoneType)] ) + elif isinstance(typ, NoneType): + return UninhabitedType() else: return typ diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index cf89518ae0c2d..2812e6f8bf791 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1008,6 +1008,22 @@ def f(x: Custom | None, y: int | None): [case testNarrowingCustomEqualityUnion4] # flags: --strict-equality --warn-unreachable from __future__ import annotations + +class Custom: + def __eq__(self, other: object) -> bool: + return True + +def f(x: Custom | None, y: Custom): + if x == y: + # We unsoundly special case None and narrow x to Custom here + reveal_type(x) # N: Revealed type is "__main__.Custom" + else: + reveal_type(x) # N: Revealed type is "__main__.Custom | None" +[builtins fixtures/primitives.pyi] + +[case testNarrowingCustomEqualityUnion5] +# flags: --strict-equality --warn-unreachable +from __future__ import annotations from typing import Any class Custom1: