From 4d3afdcc5ff58b8df55fa28318e6d60fa5bd5587 Mon Sep 17 00:00:00 2001 From: ANSHUL SINGH <72524975+ekanshul@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:52:39 +0530 Subject: [PATCH] Narrow the remaining tuple type for sequence patterns with several refutable items For a fixed-length tuple subject, the remaining type after a sequence pattern was only narrowed when at most one subpattern could fail to match. With two or more refutable subpatterns (`case True, True:` on a `tuple[bool, bool]`) the rest type stayed the whole tuple, so `match` statements over tuples of bools, enums or literals were never exhaustive and `--enable-error-code exhaustive-match` (and the missing-return check) complained. The remaining values are the ones where some item fails to match while every item before it matches, so build the rest type as the union of one tuple per refutable position: tuple[A, B] - tuple[a, b] == tuple[A - a, B] | tuple[a, B - b] Positions whose subpattern always matches keep their type. The previous single-position behaviour is the special case with one refutable item. Fixes #21968 Co-Authored-By: Claude Fable 5.1 --- mypy/checkpattern.py | 37 +++++++----- test-data/unit/check-python310.test | 87 ++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 9d02262a1530..a27bd23395b1 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -358,21 +358,28 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: new_type = UninhabitedType() else: new_type = TupleType(new_inner_types, current_type.partial_fallback) - - num_always_match = sum(is_uninhabited(typ) for typ in rest_inner_types) - if num_always_match == len(rest_inner_types): - # All subpatterns always match, so we can apply negative narrowing - rest_type = UninhabitedType() - elif num_always_match == len(rest_inner_types) - 1: - # Exactly one subpattern may conditionally match, the rest always match. - # We can apply negative narrowing to this one position. - rest_type = TupleType( - [ - curr if is_uninhabited(rest) else rest - for curr, rest in zip(inner_types, rest_inner_types) - ], - current_type.partial_fallback, - ) + # A value doesn't match the pattern if some item doesn't match its + # subpattern while all the items before it do, so what remains is the + # union of one tuple per position whose subpattern may fail to match: + # tuple[A, B] - tuple[a, b] == tuple[A - a, B] | tuple[a, B - b] + # Positions whose subpattern always matches never fail and keep their type. + matched_types = [ + curr if is_uninhabited(rest) else new + for curr, new, rest in zip(inner_types, new_inner_types, rest_inner_types) + ] + rest_items = [ + TupleType( + matched_types[:i] + [rest] + inner_types[i + 1 :], + current_type.partial_fallback, + ) + for i, rest in enumerate(rest_inner_types) + if not is_uninhabited(rest) + ] + if rest_items: + rest_type = make_simplified_union(rest_items) + else: + # All subpatterns always match, so nothing remains + rest_type = UninhabitedType() elif isinstance(current_type, TupleType): # For variadic tuples it is too tricky to match individual items like for fixed # tuples, so we instead try to narrow the entire type. diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 01e490d0da50..3b754efec022 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2083,10 +2083,95 @@ match m6: case (1, "a"): reveal_type(m6) # N: Revealed type is "tuple[Literal[1], Literal['a']]" case _: - reveal_type(m6) # N: Revealed type is "tuple[Literal[1] | Literal[2], Literal['a'] | Literal['b']]" + reveal_type(m6) # N: Revealed type is "tuple[Literal[2], Literal['a'] | Literal['b']] | tuple[Literal[1], Literal['b']]" [builtins fixtures/tuple.pyi] +[case testMatchSequencePatternNegativeNarrowingMultiplePositions] +# flags: --strict-equality --warn-unreachable +from typing import Literal, Tuple + +m1: Tuple[bool, bool] + +match m1: + case (True, True): + reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[True]]" + case (True, False): + reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[False]]" + case (False, True): + reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[True]]" + case _: + reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[False]]" + +m2: Tuple[bool, int, bool] + +match m2: + case (True, _, True): + reveal_type(m2) # N: Revealed type is "tuple[Literal[True], builtins.int, Literal[True]]" + case _: + reveal_type(m2) # N: Revealed type is "tuple[Literal[False], builtins.int, builtins.bool] | tuple[Literal[True], builtins.int, Literal[False]]" + +m3: Tuple[Literal[1], Literal[2]] + +match m3: + case (1, 3): + reveal_type(m3) # E: Statement is unreachable + case _: + reveal_type(m3) # N: Revealed type is "tuple[Literal[1], Literal[2]]" + +def f(a: bool, b: bool) -> None: + match a, b: + case True, True: + reveal_type(a) # N: Revealed type is "Literal[True]" + reveal_type(b) # N: Revealed type is "Literal[True]" + case True, False: + reveal_type(a) # N: Revealed type is "Literal[True]" + reveal_type(b) # N: Revealed type is "Literal[False]" + case False, _: + reveal_type(a) # N: Revealed type is "Literal[False]" + reveal_type(b) # N: Revealed type is "builtins.bool" + case _: + reveal_type(a) # E: Statement is unreachable +[builtins fixtures/tuple.pyi] + +[case testMatchExhaustiveTupleOfBoolsAndEnums] +# flags: --enable-error-code exhaustive-match +from enum import Enum +from typing import Tuple + +class Color(Enum): + RED = 1 + BLUE = 2 + +def f1(a: bool, b: bool) -> float: + match a, b: + case True, True: + return 1 + case True, False: + return 0.9 + case False, True: + return 0.5 + case False, False: + return 0 + +def f2(t: Tuple[Color, Color]) -> int: + match t: + case (Color.RED, Color.RED): + return 1 + case (Color.RED, Color.BLUE): + return 2 + case (Color.BLUE, _): + return 3 + +def f3(a: bool, b: bool) -> int: # E: Missing return statement + match a, b: # E: Match statement has unhandled case for values of type "tuple[Literal[False], Literal[False]]" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` + case True, _: + return 1 + case False, True: + return 2 +[builtins fixtures/tuple.pyi] + [case testMatchSequenceWildcardRefutability] # flags: --strict-equality --warn-unreachable def f1(x: int | list[int]):