From b24a17e941839e08a7429cb9b5690af79d09f594 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 4 May 2026 22:25:23 -0700 Subject: [PATCH 1/2] Fix negative narrowing for containers Fixes #21409 Relevant tests for the special case are in testNarrowingOptionalEqualsNone Co-authored-by Codex --- mypy/checker.py | 17 +++++++++++++++++ test-data/unit/check-narrowing.test | 15 +++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 4d4f376f25dd..a2ad56d46e76 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6778,6 +6778,11 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa expr_indices=[0, 1], narrowable_indices={0}, ) + if else_map and not is_tuple_of_singletons(iterable_type): + # In general, we can't do negative narrowing, since e.g. the container + # could just be empty. However, we can do negative narrowing for some + # tuples e.g. `x not in (None,)` + else_map = {} if right_index in narrowable_operand_index_to_hash: if_type, else_type = self.conditional_types_for_iterable( @@ -8793,6 +8798,18 @@ def builtin_item_type(tp: Type) -> Type | None: return None +def is_tuple_of_singletons(tp: Type) -> bool: + tp = get_proper_type(tp) + if not isinstance(tp, TupleType): + return False + for item in tp.items: + if isinstance(item, UnpackType): + return False + if not is_singleton_equality_type(get_proper_type(item)): + return False + return True + + def is_unreachable_map(map: TypeMap) -> bool: return any(isinstance(get_proper_type(v), UninhabitedType) for v in map.values()) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 6ae8a3ae11d8..e02d81afed6d 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3268,6 +3268,21 @@ def f1(x: Union[str, float], t1: list[Literal['a', 'b']], t2: list[str]): reveal_type(x) # N: Revealed type is "builtins.str | builtins.float" [builtins fixtures/primitives.pyi] +[case testNegativeNarrowingInContainer] +# flags: --warn-unreachable +from enum import Enum + +class Color(Enum): + RED = 1 + +def count(colors: list[Color]) -> None: + counts: dict[Color, int] = {} + for color in colors: + if color not in counts: + counts[color] = 0 + counts[color] += 1 +[builtins fixtures/dict.pyi] + [case testNarrowAnyWithEqualityOrContainment] # flags: --strict-equality --warn-unreachable # https://github.com/python/mypy/issues/17841 From 0a72750b6d7f0f2dfaf65f49e63a74882c6b1108 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 4 May 2026 22:32:17 -0700 Subject: [PATCH 2/2] shorter --- mypy/checker.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a2ad56d46e76..90008bf10c4e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6778,7 +6778,13 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa expr_indices=[0, 1], narrowable_indices={0}, ) - if else_map and not is_tuple_of_singletons(iterable_type): + if else_map and not ( + isinstance(p_typ := get_proper_type(iterable_type), TupleType) + and all( + is_singleton_equality_type(get_proper_type(item)) + for item in p_typ.items + ) + ): # In general, we can't do negative narrowing, since e.g. the container # could just be empty. However, we can do negative narrowing for some # tuples e.g. `x not in (None,)` @@ -8798,18 +8804,6 @@ def builtin_item_type(tp: Type) -> Type | None: return None -def is_tuple_of_singletons(tp: Type) -> bool: - tp = get_proper_type(tp) - if not isinstance(tp, TupleType): - return False - for item in tp.items: - if isinstance(item, UnpackType): - return False - if not is_singleton_equality_type(get_proper_type(item)): - return False - return True - - def is_unreachable_map(map: TypeMap) -> bool: return any(isinstance(get_proper_type(v), UninhabitedType) for v in map.values())