From f67604ad5b5b545ac84e66fca848910f62a956db Mon Sep 17 00:00:00 2001 From: Henry Su Date: Thu, 15 Oct 2026 17:13:21 -0500 Subject: [PATCH 1/2] Keep known bool value when expanding bool to a union of literals When narrowing against a bool literal, `try_expanding_sum_type_to_union` expanded `bool` into `Literal[True] | Literal[False]` even if the type already had a `last_known_value`. For `match False:` this made `case True:` look matchable and left `Literal[False]` as the remaining type after `case False:`, so the match was not treated as exhaustive. Use the last known value instead of expanding in that case. Fixes #22202. --- mypy/typeops.py | 2 ++ test-data/unit/check-python310.test | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/mypy/typeops.py b/mypy/typeops.py index 3035a15e21199..d18a04882b288 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1145,6 +1145,8 @@ class Status(Enum): target_fullname is None or typ.type.fullname == target_fullname ): if typ.type.fullname == "builtins.bool": + if typ.last_known_value is not None: + return typ.last_known_value return UnionType([LiteralType(True, typ), LiteralType(False, typ)]) if typ.type.is_enum: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 33b952f48f49d..a896f87e1cd69 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1979,6 +1979,32 @@ def b(value: bool) -> int: case False: return 3 +[case testMatchBoolLiteralSubjectExhaustive] +# flags: --strict-equality --warn-unreachable +def f() -> int: + match False: + case True: + return 1 # E: Statement is unreachable + case False: + return 0 + +def g(c: bool) -> int: + match True if c else False: + case True: + return 1 + case False: + return 0 + +def h() -> int: + match False: + case True: + return 1 # E: Statement is unreachable + case False: + return 0 + case other: + reveal_type(other) # E: Statement is unreachable + return 2 + [case testMatchMiscNonExhaustiveReturn] # flags: --strict-equality --warn-unreachable class C: From 1f78f2a1b8bbcb501e784081068db538671947b6 Mon Sep 17 00:00:00 2001 From: Henry Su Date: Fri, 23 Oct 2026 00:17:55 -0500 Subject: [PATCH 2/2] Only use a known bool value in singleton match patterns Expanding every bool with a last_known_value to that literal also narrowed truthiness checks on Final flags (if DEBUG:), reporting their bodies as unreachable, which broke the self check. Apply the known value only when narrowing a match subject against True/False. --- mypy/checkpattern.py | 18 +++++++++++++++++- mypy/typeops.py | 2 -- test-data/unit/check-python310.test | 11 +++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 81904bc245e01..a1b069bb324f8 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -216,7 +216,9 @@ def visit_value_pattern(self, o: ValuePattern) -> PatternType: return PatternType(ok_type, rest_type, {}) def visit_singleton_pattern(self, o: SingletonPattern) -> PatternType: - current_type = self.type_context[-1] + # A bool subject with a known value (e.g. `match False:`) must not be + # expanded to both literals, or `case True:` would look reachable. + current_type = known_bool_value(self.type_context[-1]) value: bool | None = o.value if isinstance(value, bool): typ = self.chk.expr_checker.infer_literal_expr_type(value, "builtins.bool") @@ -881,3 +883,17 @@ def get_type_range(typ: Type) -> TypeRange: def is_uninhabited(typ: Type) -> bool: return isinstance(get_proper_type(typ), UninhabitedType) + + +def known_bool_value(typ: Type) -> Type: + """Replace bool types that have a known value with that literal.""" + p_typ = get_proper_type(typ) + if isinstance(p_typ, UnionType): + return UnionType.make_union([known_bool_value(t) for t in p_typ.items], p_typ.line) + if ( + isinstance(p_typ, Instance) + and p_typ.type.fullname == "builtins.bool" + and p_typ.last_known_value is not None + ): + return p_typ.last_known_value + return typ diff --git a/mypy/typeops.py b/mypy/typeops.py index d18a04882b288..3035a15e21199 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1145,8 +1145,6 @@ class Status(Enum): target_fullname is None or typ.type.fullname == target_fullname ): if typ.type.fullname == "builtins.bool": - if typ.last_known_value is not None: - return typ.last_known_value return UnionType([LiteralType(True, typ), LiteralType(False, typ)]) if typ.type.is_enum: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a896f87e1cd69..db0715885f1f8 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2005,6 +2005,17 @@ def h() -> int: reveal_type(other) # E: Statement is unreachable return 2 +[case testBoolFinalFlagTruthinessNotNarrowedToKnownValue] +# flags: --warn-unreachable +from typing import Final + +DEBUG: Final = False + +def f() -> None: + if DEBUG: + x = 1 +[builtins fixtures/bool.pyi] + [case testMatchMiscNonExhaustiveReturn] # flags: --strict-equality --warn-unreachable class C: