From 2b1f15cc1dc9eb3519e432d132d1f998ef91eaab Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 2 Apr 2026 15:17:51 -0700 Subject: [PATCH 1/2] Fix narrowing with chained comparison Fixes #21149 --- mypy/checker.py | 4 ++-- test-data/unit/check-narrowing.test | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7d0b5dbde09d8..5c9bc5fe58631 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6674,8 +6674,8 @@ def comparison_type_narrowing_helper(self, node: ComparisonExpr) -> tuple[TypeMa # If we have found non-trivial restrictions from the regular comparisons, # then return soon. Otherwise try to infer restrictions involving `len(x)`. # TODO: support regular and len() narrowing in the same chain. - if any(m != ({}, {}) for m in partial_type_maps): - return reduce_conditional_maps(partial_type_maps) + if any(len(m[0]) or len(m[1]) for m in partial_type_maps): + return reduce_conditional_maps(partial_type_maps, use_meet=True) else: # Use meet for `and` maps to get correct results for chained checks # like `if 1 < len(x) < 4: ...` diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 93a7207288761..2b280e70738bb 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3264,6 +3264,20 @@ def bad_but_should_pass(has_key: bool, key: bool, s: tuple[bool, ...]) -> None: reveal_type(key) # N: Revealed type is "builtins.bool" [builtins fixtures/primitives.pyi] +[case testNarrowChainedComparisonMeet] +# flags: --strict-equality --warn-unreachable +from __future__ import annotations + +def f1(a: str | None, b: str | None) -> None: + if None is not a == b: + reveal_type(a) # N: Revealed type is "builtins.str" + reveal_type(b) # N: Revealed type is "builtins.str | None" + + if (None is not a) and (a == b): + reveal_type(a) # N: Revealed type is "builtins.str" + reveal_type(b) # N: Revealed type is "builtins.str" +[builtins fixtures/primitives.pyi] + [case testNarrowTypeObject] # flags: --strict-equality --warn-unreachable from typing import Any From 36e50c9a00edbcafcc1523ebff059eb7dbdba1d4 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Fri, 3 Apr 2026 13:11:49 -0700 Subject: [PATCH 2/2] more tests --- test-data/unit/check-narrowing.test | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 2b280e70738bb..0608d2a0b2f16 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -3267,6 +3267,7 @@ def bad_but_should_pass(has_key: bool, key: bool, s: tuple[bool, ...]) -> None: [case testNarrowChainedComparisonMeet] # flags: --strict-equality --warn-unreachable from __future__ import annotations +from typing import Any def f1(a: str | None, b: str | None) -> None: if None is not a == b: @@ -3276,6 +3277,24 @@ def f1(a: str | None, b: str | None) -> None: if (None is not a) and (a == b): reveal_type(a) # N: Revealed type is "builtins.str" reveal_type(b) # N: Revealed type is "builtins.str" + +def f2(a: Any | None, b: str | None) -> None: + if None is not a == b: + reveal_type(a) # N: Revealed type is "Any" + reveal_type(b) # N: Revealed type is "builtins.str | None" + + if (None is not a) and (a == b): + reveal_type(a) # N: Revealed type is "Any" + reveal_type(b) # N: Revealed type is "builtins.str | None" + +def f3(a: str | None, b: Any | None) -> None: + if None is not a == b: + reveal_type(a) # N: Revealed type is "builtins.str" + reveal_type(b) # N: Revealed type is "Any | builtins.str | None" + + if (None is not a) and (a == b): + reveal_type(a) # N: Revealed type is "builtins.str" + reveal_type(b) # N: Revealed type is "Any | builtins.str" [builtins fixtures/primitives.pyi] [case testNarrowTypeObject]