Skip to content

Commit ce7669e

Browse files
authored
Improve reachability in narrowing logic (#20660)
Improves #17372 Fixes #17045 Improves #14965 Fixes this comment #9003 (comment)
1 parent ba41d11 commit ce7669e

File tree

10 files changed

+85
-62
lines changed

10 files changed

+85
-62
lines changed

mypy/checker.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6799,8 +6799,15 @@ def narrow_type_by_identity_equality(
67996799
# However, for non-value targets, we cannot do this narrowing,
68006800
# and so we ignore else_map
68016801
# e.g. if (x: str | None) != (y: str), we cannot narrow x to None
6802-
# TODO: this reachability gate is incorrect and should be removed
6803-
if not is_unreachable_map(if_map):
6802+
6803+
# It is correct to always narrow here. It improves behaviour on tests and
6804+
# detects many inaccurate type annotations on primer.
6805+
# However, because mypy does not currently check unreachable code, it feels
6806+
# risky to narrow to unreachable without --warn-unreachable.
6807+
# See also this specific primer comment, where I force primer to run with
6808+
# --warn-unreachable to see what code we would stop checking:
6809+
# https://github.com/python/mypy/pull/20660#issuecomment-3865794148
6810+
if self.options.warn_unreachable or not is_unreachable_map(if_map):
68046811
all_if_maps.append(if_map)
68056812

68066813
# Handle narrowing for operands with custom __eq__ methods specially

mypy/server/aststrip.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ def visit_class_def(self, node: ClassDef) -> None:
137137
node.base_type_exprs.extend(node.removed_base_type_exprs)
138138
node.removed_base_type_exprs = []
139139
node.defs.body = [
140-
s for s in node.defs.body if s not in to_delete # type: ignore[comparison-overlap]
140+
s
141+
for s in node.defs.body
142+
if s not in to_delete # type: ignore[comparison-overlap, redundant-expr]
141143
]
142144
with self.enter_class(node.info):
143145
super().visit_class_def(node)

mypy/typeshed/stubs/mypy-extensions/mypy_extensions.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class i64:
111111
def __ge__(self, x: i64) -> bool: ...
112112
def __gt__(self, x: i64) -> bool: ...
113113
def __index__(self) -> int: ...
114+
def __eq__(self, x: object) -> bool: ...
114115

115116
class i32:
116117
@overload
@@ -146,6 +147,7 @@ class i32:
146147
def __ge__(self, x: i32) -> bool: ...
147148
def __gt__(self, x: i32) -> bool: ...
148149
def __index__(self) -> int: ...
150+
def __eq__(self, x: object) -> bool: ...
149151

150152
class i16:
151153
@overload
@@ -181,6 +183,7 @@ class i16:
181183
def __ge__(self, x: i16) -> bool: ...
182184
def __gt__(self, x: i16) -> bool: ...
183185
def __index__(self) -> int: ...
186+
def __eq__(self, x: object) -> bool: ...
184187

185188
class u8:
186189
@overload
@@ -216,3 +219,4 @@ class u8:
216219
def __ge__(self, x: u8) -> bool: ...
217220
def __gt__(self, x: u8) -> bool: ...
218221
def __index__(self) -> int: ...
222+
def __eq__(self, x: object) -> bool: ...

test-data/unit/check-isinstance.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,7 +2138,7 @@ x: List[str]
21382138
y: Optional[int]
21392139

21402140
if y in x:
2141-
reveal_type(y) # N: Revealed type is "builtins.int | None"
2141+
reveal_type(y) # E: Statement is unreachable
21422142
else:
21432143
reveal_type(y) # N: Revealed type is "builtins.int | None"
21442144
[builtins fixtures/list.pyi]
@@ -2154,7 +2154,7 @@ nested_any: List[List[Any]]
21542154
if lst in nested_any:
21552155
reveal_type(lst) # N: Revealed type is "builtins.list[builtins.int]"
21562156
if x in nested_any:
2157-
reveal_type(x) # N: Revealed type is "builtins.int | None"
2157+
reveal_type(x) # E: Statement is unreachable
21582158
[builtins fixtures/list.pyi]
21592159

21602160
[case testNarrowTypeAfterInTuple]

test-data/unit/check-narrowing.test

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,8 +1127,8 @@ S = Callable[[B], "T"]
11271127

11281128
def f(x: S, y: T):
11291129
if x == y: # E: Unsupported left operand type for == ("Callable[[B], T]")
1130-
reveal_type(x) # N: Revealed type is "def (__main__.B) -> def (__main__.A) -> ..."
1131-
reveal_type(y) # N: Revealed type is "def (__main__.A) -> def (__main__.B) -> ..."
1130+
reveal_type(x) # E: Statement is unreachable
1131+
reveal_type(y)
11321132
else:
11331133
reveal_type(x) # N: Revealed type is "def (__main__.B) -> def (__main__.A) -> ..."
11341134
reveal_type(y) # N: Revealed type is "def (__main__.A) -> def (__main__.B) -> ..."
@@ -2808,7 +2808,7 @@ while True:
28082808
break
28092809
x = str()
28102810
if x == int(): # E: Non-overlapping equality check (left operand type: "str", right operand type: "int")
2811-
break
2811+
break # E: Statement is unreachable
28122812
[builtins fixtures/primitives.pyi]
28132813

28142814
[case testAvoidFalseNonOverlappingEqualityCheckInLoop2]
@@ -2821,7 +2821,7 @@ class C: ...
28212821
x = A()
28222822
while True:
28232823
if x == C(): # E: Non-overlapping equality check (left operand type: "A | B", right operand type: "C")
2824-
break
2824+
break # E: Statement is unreachable
28252825
x = B()
28262826
[builtins fixtures/primitives.pyi]
28272827

@@ -3213,7 +3213,7 @@ def narrow_tuple(x: Literal['c'], overlap: list[Literal['b', 'c']], no_overlap:
32133213
reveal_type(x) # N: Revealed type is "Literal['c']"
32143214

32153215
if x in no_overlap:
3216-
reveal_type(x) # N: Revealed type is "Literal['c']"
3216+
reveal_type(x) # E: Statement is unreachable
32173217
else:
32183218
reveal_type(x) # N: Revealed type is "Literal['c']"
32193219
[builtins fixtures/tuple.pyi]
@@ -3255,8 +3255,8 @@ def f2(x: Any) -> None:
32553255

32563256
def bad_compare_has_key(has_key: bool, key: str, s: tuple[str, ...]) -> None:
32573257
if has_key == key in s: # E: Non-overlapping equality check (left operand type: "bool", right operand type: "str")
3258-
reveal_type(has_key) # N: Revealed type is "builtins.bool"
3259-
reveal_type(key) # N: Revealed type is "builtins.str"
3258+
reveal_type(has_key) # E: Statement is unreachable
3259+
reveal_type(key)
32603260

32613261
def bad_but_should_pass(has_key: bool, key: bool, s: tuple[bool, ...]) -> None:
32623262
if has_key == key in s:
@@ -3623,6 +3623,7 @@ def bar(y: Any):
36233623
reveal_type(y) # N: Revealed type is "Any"
36243624
[builtins fixtures/dict-full.pyi]
36253625

3626+
36263627
[case testNarrowTypeVarType]
36273628
# flags: --strict-equality --warn-unreachable
36283629
from typing import TypeVar
@@ -3653,14 +3654,14 @@ TargetType = TypeVar("TargetType", int, float, str)
36533654
# TODO: this behaviour is incorrect, it will be fixed by improving reachability
36543655
def convert_type(target_type: Type[TargetType]) -> TargetType:
36553656
if target_type == str:
3656-
return str() # E: Incompatible return value type (got "str", expected "int") \
3657-
# E: Incompatible return value type (got "str", expected "float")
3657+
return str()
36583658
if target_type == int:
3659-
return int() # E: Incompatible return value type (got "int", expected "str")
3659+
return int()
36603660
if target_type == float:
3661-
return float() # E: Incompatible return value type (got "float", expected "int") \
3662-
# E: Incompatible return value type (got "float", expected "str")
3661+
return float() # E: Incompatible return value type (got "float", expected "int")
36633662
raise
3663+
3664+
36643665
[builtins fixtures/primitives.pyi]
36653666

36663667

@@ -3841,16 +3842,16 @@ class A2(A): ...
38413842

38423843
def check_a(base: A, a1: A1, a2: A2):
38433844
if a1 == a2: # E: Non-overlapping equality check (left operand type: "A1", right operand type: "A2")
3844-
reveal_type(a1) # N: Revealed type is "__main__.A1"
3845-
reveal_type(a2) # N: Revealed type is "__main__.A2"
3845+
reveal_type(a1) # E: Statement is unreachable
3846+
reveal_type(a2)
38463847

38473848
if a1 == base:
38483849
# We do narrow base
38493850
reveal_type(a1) # N: Revealed type is "__main__.A1"
38503851
reveal_type(base) # N: Revealed type is "__main__.A1"
38513852
if a2 == base: # E: Non-overlapping equality check (left operand type: "A2", right operand type: "A1")
3852-
reveal_type(a2) # N: Revealed type is "__main__.A2"
3853-
reveal_type(base) # N: Revealed type is "__main__.A1"
3853+
reveal_type(a2) # E: Statement is unreachable
3854+
reveal_type(base)
38543855
[builtins fixtures/primitives.pyi]
38553856

38563857

test-data/unit/check-optional.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,11 +493,11 @@ from typing import Optional
493493

494494
def main(x: Optional[str]):
495495
if x == 0:
496-
reveal_type(x) # N: Revealed type is "builtins.str | None"
496+
reveal_type(x) # E: Statement is unreachable
497497
else:
498498
reveal_type(x) # N: Revealed type is "builtins.str | None"
499499
if x is 0:
500-
reveal_type(x) # N: Revealed type is "builtins.str | None"
500+
reveal_type(x) # E: Statement is unreachable
501501
else:
502502
reveal_type(x) # N: Revealed type is "builtins.str | None"
503503
[builtins fixtures/ops.pyi]

test-data/unit/check-python310.test

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ m: A
9191

9292
match m:
9393
case b.b:
94-
reveal_type(m) # N: Revealed type is "__main__.A"
94+
reveal_type(m) # E: Statement is unreachable
9595
[file b.py]
9696
class B: ...
9797
b: B
@@ -104,7 +104,7 @@ m: int
104104

105105
match m:
106106
case b.b:
107-
reveal_type(m) # N: Revealed type is "builtins.int"
107+
reveal_type(m) # E: Statement is unreachable
108108
[file b.py]
109109
b: str
110110
[builtins fixtures/primitives.pyi]
@@ -3049,7 +3049,7 @@ def x() -> tuple[Literal["test"]]: ...
30493049

30503050
match x():
30513051
case (x,) if x == "test": # E: Incompatible types in capture pattern (pattern captures type "Literal['test']", variable has type "Callable[[], tuple[Literal['test']]]")
3052-
reveal_type(x) # N: Revealed type is "def () -> tuple[Literal['test']]"
3052+
reveal_type(x) # E: Statement is unreachable
30533053
case foo:
30543054
foo
30553055

test-data/unit/check-tuples.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1540,7 +1540,7 @@ class B: pass
15401540

15411541
def f1(possibles: Tuple[int, Tuple[A]], x: Optional[Tuple[B]]):
15421542
if x in possibles:
1543-
reveal_type(x) # N: Revealed type is "tuple[__main__.B]"
1543+
reveal_type(x) # E: Statement is unreachable
15441544
else:
15451545
reveal_type(x) # N: Revealed type is "tuple[__main__.B] | None"
15461546

test-data/unit/check-unreachable-code.test

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -424,24 +424,22 @@ x = 1
424424
[case testCustomSysVersionInfo]
425425
# flags: --python-version 3.11
426426
import sys
427-
if sys.version_info == (3, 11):
427+
if sys.version_info >= (3, 11):
428428
x = "foo"
429429
else:
430430
x = 3
431431
reveal_type(x) # N: Revealed type is "builtins.str"
432432
[builtins fixtures/ops.pyi]
433-
[out]
434433

435434
[case testCustomSysVersionInfo2]
436435
# flags: --python-version 3.11
437436
import sys
438-
if sys.version_info == (3, 6):
437+
if sys.version_info >= (3, 12):
439438
x = "foo"
440439
else:
441440
x = 3
442441
reveal_type(x) # N: Revealed type is "builtins.int"
443442
[builtins fixtures/ops.pyi]
444-
[out]
445443

446444
[case testCustomSysPlatform]
447445
# flags: --platform linux
@@ -894,42 +892,53 @@ def baz(x: int) -> int:
894892
import sys
895893
from typing import TYPE_CHECKING
896894

897-
x: int
898-
if TYPE_CHECKING:
899-
reveal_type(x) # N: Revealed type is "builtins.int"
900-
else:
901-
reveal_type(x)
895+
def f1(x: int) -> None:
896+
if TYPE_CHECKING:
897+
reveal_type(x) # N: Revealed type is "builtins.int"
898+
else:
899+
reveal_type(x)
902900

903-
if not TYPE_CHECKING:
904-
reveal_type(x)
905-
else:
906-
reveal_type(x) # N: Revealed type is "builtins.int"
901+
def f2(x: int) -> None:
902+
if not TYPE_CHECKING:
903+
reveal_type(x)
904+
else:
905+
reveal_type(x) # N: Revealed type is "builtins.int"
907906

908-
if sys.platform == 'darwin':
909-
reveal_type(x)
910-
else:
911-
reveal_type(x) # N: Revealed type is "builtins.int"
907+
def f3(x: int) -> None:
908+
if sys.platform == 'darwin':
909+
reveal_type(x)
910+
else:
911+
reveal_type(x) # N: Revealed type is "builtins.int"
912912

913-
if sys.platform == 'win32':
914-
reveal_type(x) # N: Revealed type is "builtins.int"
915-
else:
916-
reveal_type(x)
913+
def f4(x: int) -> None:
914+
if sys.platform == 'win32':
915+
reveal_type(x) # N: Revealed type is "builtins.int"
916+
else:
917+
reveal_type(x)
917918

918-
if sys.version_info == (2, 7):
919-
reveal_type(x)
920-
else:
921-
reveal_type(x) # N: Revealed type is "builtins.int"
919+
def f5(x: int) -> None:
920+
if sys.version_info == (2, 7):
921+
reveal_type(x)
922+
else:
923+
reveal_type(x) # N: Revealed type is "builtins.int"
922924

923-
if sys.version_info == (3, 9):
924-
reveal_type(x) # N: Revealed type is "builtins.int"
925-
else:
926-
reveal_type(x)
925+
def f6(x: int) -> None:
926+
if sys.version_info >= (3, 9):
927+
reveal_type(x) # N: Revealed type is "builtins.int"
928+
else:
929+
reveal_type(x)
927930

928-
FOOBAR = ""
929-
if FOOBAR:
930-
reveal_type(x)
931-
else:
932-
reveal_type(x) # N: Revealed type is "builtins.int"
931+
if sys.version_info >= (3, 10):
932+
reveal_type(x)
933+
else:
934+
reveal_type(x) # N: Revealed type is "builtins.int"
935+
936+
def f7(x: int) -> None:
937+
FOOBAR = ""
938+
if FOOBAR:
939+
reveal_type(x)
940+
else:
941+
reveal_type(x) # N: Revealed type is "builtins.int"
933942
[builtins fixtures/ops.pyi]
934943
[typing fixtures/typing-medium.pyi]
935944

test-data/unit/cmdline.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ python_version = 3.14
374374
# cmd: mypy main.py
375375
[file main.py]
376376
import sys
377-
if sys.version_info == (3, 9): # Update here when bumping the min Python version!
377+
if sys.version_info < (3, 10): # Update here when bumping the min Python version!
378378
reveal_type("good")
379379
[file mypy.ini]
380380
\[mypy]

0 commit comments

Comments
 (0)