From 7a490d21167b7372bb2401ab46df540e9a371208 Mon Sep 17 00:00:00 2001 From: Dipanshu Ambilkar Date: Wed, 28 Oct 2026 22:50:27 +0530 Subject: [PATCH] Fix narrowing of bool literal match subjects (#22202) An expression such as `False` has type `bool` with a known value, stored as `last_known_value`. When narrowing against a bool literal pattern, `conditional_types()` expands the instance into `Literal[True] | Literal[False]` so that one member can be removed, but the expansion dropped the known value. As a result, `match False: case False:` left `Literal[True]` as the remaining type, the match was not considered exhaustive, and a following `case other` captured `Literal[False]` even though the subject could only be `False`. Keep the known value when expanding a sum type: if the instance already has a last known value, expand to that literal instead of the full union. The existing behaviour for plain `bool` and enum instances is unchanged. --- mypy/typeops.py | 6 ++++++ test-data/unit/check-python310.test | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/mypy/typeops.py b/mypy/typeops.py index eb9d0d1474b8b..4a694b0a88a67 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1153,6 +1153,12 @@ class Status(Enum): if isinstance(typ, Instance) and ( target_fullname is None or typ.type.fullname == target_fullname ): + if typ.last_known_value is not None: + # The expression is already known to hold this specific value, so keep it: expanding + # to all members of the sum type would forget it and break narrowing, e.g. matching a + # ``False`` literal subject against ``case False`` would not be exhaustive. + return typ.last_known_value + if typ.type.fullname == "builtins.bool": return UnionType([LiteralType(True, typ), LiteralType(False, typ)]) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 33b952f48f49d..a75c4fb9efced 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -50,6 +50,34 @@ def foo(m: bool, x: int): reveal_type(m) # N: Revealed type is "builtins.bool" [builtins fixtures/primitives.pyi] +[case testMatchBoolLiteralSubjectIsExhaustive] +# flags: --strict-equality --warn-unreachable + +def f() -> int: + match False: + case True: + return 1 # E: Statement is unreachable + case False: + return 0 + +def g(b: bool) -> int: + match b: + 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 +[builtins fixtures/primitives.pyi] + [case testMatchLiteralPatternUnreachable] # flags: --strict-equality --warn-unreachable m: int