From 48ef44b68c22f3ed17312fb183a6be2abc4f21f1 Mon Sep 17 00:00:00 2001 From: calm329 Date: Tue, 6 Jan 2026 14:24:33 -0800 Subject: [PATCH 1/6] Fix false positive unreachable with Any in equality comparison --- mypy/checker.py | 4 ++++ test-data/unit/check-unreachable-code.test | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 0fb37450a015..9e761feaeb5a 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7072,6 +7072,10 @@ def refine_away_none_in_comparison( non_optional_types = [] for i in chain_indices: typ = operand_types[i] + # Skip Any types - they could be None, so comparing with them + # shouldn't narrow away None from other operands. + if isinstance(get_proper_type(typ), AnyType): + continue if not is_overlapping_none(typ): non_optional_types.append(typ) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index 98c676dbf42b..a15c3992a733 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1650,3 +1650,15 @@ def x() -> None: main:4: error: Statement is unreachable if 5: ^~~~~ + +[case testNoFalseUnreachableWithAnyEqualityNarrowing] +# flags: --warn-unreachable +# Regression test for https://github.com/python/mypy/issues/20532 +from typing import Any, Optional + +def main(contents: Any, commit: Optional[str]) -> None: + if ( + contents.get("commit") == commit + and (commit is not None or 1) + ): + pass From 4c9dd7077201cb962d610560ae2b78f04ba5ae17 Mon Sep 17 00:00:00 2001 From: calm329 Date: Tue, 6 Jan 2026 14:45:50 -0800 Subject: [PATCH 2/6] Fix is_overlapping_none to handle Any types --- mypy/checker.py | 4 ---- mypy/types_utils.py | 6 ++++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9e761feaeb5a..0fb37450a015 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7072,10 +7072,6 @@ def refine_away_none_in_comparison( non_optional_types = [] for i in chain_indices: typ = operand_types[i] - # Skip Any types - they could be None, so comparing with them - # shouldn't narrow away None from other operands. - if isinstance(get_proper_type(typ), AnyType): - continue if not is_overlapping_none(typ): non_optional_types.append(typ) diff --git a/mypy/types_utils.py b/mypy/types_utils.py index 3c1dcb427f29..824af5a22f46 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -123,8 +123,10 @@ def is_generic_instance(tp: Type) -> bool: def is_overlapping_none(t: Type) -> bool: t = get_proper_type(t) - return isinstance(t, NoneType) or ( - isinstance(t, UnionType) and any(isinstance(get_proper_type(e), NoneType) for e in t.items) + return ( + isinstance(t, NoneType) + or isinstance(t, AnyType) + or (isinstance(t, UnionType) and any(isinstance(get_proper_type(e), NoneType) for e in t.items)) ) From e922a7078b43a371b17fdc5fcd407d46c8045ec1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:48:03 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/types_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mypy/types_utils.py b/mypy/types_utils.py index 824af5a22f46..a3044080493c 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -126,7 +126,10 @@ def is_overlapping_none(t: Type) -> bool: return ( isinstance(t, NoneType) or isinstance(t, AnyType) - or (isinstance(t, UnionType) and any(isinstance(get_proper_type(e), NoneType) for e in t.items)) + or ( + isinstance(t, UnionType) + and any(isinstance(get_proper_type(e), NoneType) for e in t.items) + ) ) From a298d67d5e152aae204f84d90517f49413257b1a Mon Sep 17 00:00:00 2001 From: calm329 Date: Tue, 6 Jan 2026 14:52:52 -0800 Subject: [PATCH 4/6] Fix ruff SIM101: merge isinstance calls --- mypy/types_utils.py | 9 ++------- test-data/unit/check-unreachable-code.test | 7 +------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/mypy/types_utils.py b/mypy/types_utils.py index a3044080493c..47c97c0a76ae 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -123,13 +123,8 @@ def is_generic_instance(tp: Type) -> bool: def is_overlapping_none(t: Type) -> bool: t = get_proper_type(t) - return ( - isinstance(t, NoneType) - or isinstance(t, AnyType) - or ( - isinstance(t, UnionType) - and any(isinstance(get_proper_type(e), NoneType) for e in t.items) - ) + return isinstance(t, (NoneType, AnyType)) or ( + isinstance(t, UnionType) and any(isinstance(get_proper_type(e), NoneType) for e in t.items) ) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index a15c3992a733..e7ef9538d4f6 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1653,12 +1653,7 @@ main:4: error: Statement is unreachable [case testNoFalseUnreachableWithAnyEqualityNarrowing] # flags: --warn-unreachable -# Regression test for https://github.com/python/mypy/issues/20532 from typing import Any, Optional - def main(contents: Any, commit: Optional[str]) -> None: - if ( - contents.get("commit") == commit - and (commit is not None or 1) - ): + if contents.get("commit") == commit and (commit is not None or 1): pass From 3416461f575f311a7e2efd6434ae19785b327de9 Mon Sep 17 00:00:00 2001 From: calm329 Date: Tue, 6 Jan 2026 15:22:39 -0800 Subject: [PATCH 5/6] Add reveal_type to test to verify commit stays str | None --- test-data/unit/check-unreachable-code.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-unreachable-code.test b/test-data/unit/check-unreachable-code.test index e7ef9538d4f6..82f79269e4ad 100644 --- a/test-data/unit/check-unreachable-code.test +++ b/test-data/unit/check-unreachable-code.test @@ -1655,5 +1655,5 @@ main:4: error: Statement is unreachable # flags: --warn-unreachable from typing import Any, Optional def main(contents: Any, commit: Optional[str]) -> None: - if contents.get("commit") == commit and (commit is not None or 1): - pass + if contents.get("commit") == commit: + reveal_type(commit) # N: Revealed type is "builtins.str | None" From 419c936ed6a95d91c11ce1db1f4664f30a332143 Mon Sep 17 00:00:00 2001 From: calm329 Date: Tue, 6 Jan 2026 15:41:53 -0800 Subject: [PATCH 6/6] Revert changes to the targeted fix --- mypy/checker.py | 2 ++ mypy/types_utils.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 0fb37450a015..b356dba0e06b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7072,6 +7072,8 @@ def refine_away_none_in_comparison( non_optional_types = [] for i in chain_indices: typ = operand_types[i] + if isinstance(get_proper_type(typ), AnyType): + continue if not is_overlapping_none(typ): non_optional_types.append(typ) diff --git a/mypy/types_utils.py b/mypy/types_utils.py index 47c97c0a76ae..3c1dcb427f29 100644 --- a/mypy/types_utils.py +++ b/mypy/types_utils.py @@ -123,7 +123,7 @@ def is_generic_instance(tp: Type) -> bool: def is_overlapping_none(t: Type) -> bool: t = get_proper_type(t) - return isinstance(t, (NoneType, AnyType)) or ( + return isinstance(t, NoneType) or ( isinstance(t, UnionType) and any(isinstance(get_proper_type(e), NoneType) for e in t.items) )