From 478564dd11f1f3074954d5fda80e67122b559fb1 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 1 Apr 2026 13:08:25 -0700 Subject: [PATCH 1/2] Avoid narrowing to unreachable at module level Helps with confusing symptoms in #21132 --- mypy/checker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7d0b5dbde09d8..754a407331ed4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -6803,11 +6803,14 @@ def narrow_type_by_identity_equality( # It is correct to always narrow here. It improves behaviour on tests and # detects many inaccurate type annotations on primer. # However, because mypy does not currently check unreachable code, it feels - # risky to narrow to unreachable without --warn-unreachable. + # risky to narrow to unreachable without --warn-unreachable or not + # at module level # See also this specific primer comment, where I force primer to run with # --warn-unreachable to see what code we would stop checking: # https://github.com/python/mypy/pull/20660#issuecomment-3865794148 - if self.options.warn_unreachable or not is_unreachable_map(if_map): + if ( + self.options.warn_unreachable and len(self.scope.stack) != 1 + ) or not is_unreachable_map(if_map): all_if_maps.append(if_map) # Handle narrowing for operands with custom __eq__ methods specially From 7aef2d4752a6f8a923242439f0b920eba89521a9 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 1 Apr 2026 13:28:17 -0700 Subject: [PATCH 2/2] tests --- test-data/unit/check-isinstance.test | 25 ++++++++++------------- test-data/unit/check-narrowing.test | 24 +++++++++++----------- test-data/unit/check-python310.test | 30 ++++++++++++++-------------- 3 files changed, 37 insertions(+), 42 deletions(-) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 0b20d5911151f..310145dfc4efe 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -2134,27 +2134,22 @@ else: # flags: --warn-unreachable from typing import List, Optional -x: List[str] -y: Optional[int] - -if y in x: - reveal_type(y) # E: Statement is unreachable -else: - reveal_type(y) # N: Revealed type is "builtins.int | None" +def f(x: List[str], y: Optional[int]) -> None: + if y in x: + reveal_type(y) # E: Statement is unreachable + else: + reveal_type(y) # N: Revealed type is "builtins.int | None" [builtins fixtures/list.pyi] [case testNarrowTypeAfterInListNested] # flags: --warn-unreachable from typing import List, Optional, Any -x: Optional[int] -lst: Optional[List[int]] -nested_any: List[List[Any]] - -if lst in nested_any: - reveal_type(lst) # N: Revealed type is "builtins.list[builtins.int]" -if x in nested_any: - reveal_type(x) # E: Statement is unreachable +def f(x: Optional[int], lst: Optional[List[int]], nested_any: List[List[Any]]) -> None: + if lst in nested_any: + reveal_type(lst) # N: Revealed type is "builtins.list[builtins.int]" + if x in nested_any: + reveal_type(x) # E: Statement is unreachable [builtins fixtures/list.pyi] [case testNarrowTypeAfterInTuple] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 93a7207288761..089776e539f4f 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2802,13 +2802,13 @@ while x is not None and b(): [case testAvoidFalseNonOverlappingEqualityCheckInLoop1] # flags: --allow-redefinition-new --local-partial-types --strict-equality --warn-unreachable -x = 1 -while True: - if x == str(): - break - x = str() - if x == int(): # E: Non-overlapping equality check (left operand type: "str", right operand type: "int") - break # E: Statement is unreachable +def f(x: int) -> None: + while True: + if x == str(): + break + x = str() + if x == int(): # E: Non-overlapping equality check (left operand type: "str", right operand type: "int") + break # E: Statement is unreachable [builtins fixtures/primitives.pyi] [case testAvoidFalseNonOverlappingEqualityCheckInLoop2] @@ -2818,11 +2818,11 @@ class A: ... class B: ... class C: ... -x = A() -while True: - if x == C(): # E: Non-overlapping equality check (left operand type: "A | B", right operand type: "C") - break # E: Statement is unreachable - x = B() +def f(x: A) -> None: + while True: + if x == C(): # E: Non-overlapping equality check (left operand type: "A | B", right operand type: "C") + break # E: Statement is unreachable + x = B() [builtins fixtures/primitives.pyi] [case testAvoidFalseNonOverlappingEqualityCheckInLoop3] diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index ff334ff6c692b..e2074d347428e 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -87,11 +87,11 @@ b: int import b class A: ... -m: A -match m: - case b.b: - reveal_type(m) # E: Statement is unreachable +def f(m: A) -> None: + match m: + case b.b: + reveal_type(m) # E: Statement is unreachable [file b.py] class B: ... b: B @@ -100,11 +100,10 @@ b: B # flags: --strict-equality --warn-unreachable import b -m: int - -match m: - case b.b: - reveal_type(m) # E: Statement is unreachable +def f(m: int) -> None: + match m: + case b.b: + reveal_type(m) # E: Statement is unreachable [file b.py] b: str [builtins fixtures/primitives.pyi] @@ -3151,13 +3150,14 @@ def nested_in_dict(d: dict[str, Any]) -> int: # flags: --warn-unreachable from typing import Literal -def x() -> tuple[Literal["test"]]: ... +def f() -> None: + def x() -> tuple[Literal["test"]]: ... -match x(): - case (x,) if x == "test": # E: Incompatible types in capture pattern (pattern captures type "Literal['test']", variable has type "Callable[[], tuple[Literal['test']]]") - reveal_type(x) # E: Statement is unreachable - case foo: - foo + match x(): + case (x,) if x == "test": # E: Incompatible types in capture pattern (pattern captures type "Literal['test']", variable has type "Callable[[], tuple[Literal['test']]]") + reveal_type(x) # E: Statement is unreachable + case foo: + foo [builtins fixtures/dict.pyi]