From 003686d77be82254718a8cd3eef51022bfacf35e Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 16:29:03 +0000 Subject: [PATCH 01/11] Add failing test for #19599 --- test-data/unit/check-python310.test | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index f264167cb067..a246978f0046 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1559,6 +1559,16 @@ match m6: [builtins fixtures/tuple.pyi] +[case testMatchTupleFields] +x: tuple[int, str] | None +match x: + case (a, b): + reveal_type(a) # N: Revealed type is "int" + reveal_type(b) # N: Revealed type is "str" + +[builtins fixtures/tuple.pyi] + + [case testMatchEnumSingleChoice] from enum import Enum from typing import NoReturn From 6a179f77d580cb6ebcc2814942230db096f5bb9c Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 13:21:45 -0400 Subject: [PATCH 02/11] Add more comphrenensive tests Inspired by @Copilot in https://github.com/saulshanabrook/mypy/pull/2/commits/35ae08916b980e7a09a935de14558ec434aaca58 --- test-data/unit/check-python310.test | 30 +++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a246978f0046..49889a92cc12 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1559,12 +1559,30 @@ match m6: [builtins fixtures/tuple.pyi] -[case testMatchTupleFields] -x: tuple[int, str] | None -match x: - case (a, b): - reveal_type(a) # N: Revealed type is "int" - reveal_type(b) # N: Revealed type is "str" +[case testMatchTupleUnions] +m1: tuple[int, str] | None +match m1: + case (a1, b1): + reveal_type(a1) # N: Revealed type is "int" + reveal_type(b1) # N: Revealed type is "str" + +m2: tuple[int, str] | tuple[float, str] +match m2: + case (a2, b2): + reveal_type(a2) # N: Revealed type is "Union[int, float]" + reveal_type(b2) # N: Revealed type is "str" + +m3: tuple[int] | tuple[float, str] +match m3: + case (a3, b3): + reveal_type(a3) # N: Revealed type is "float" + reveal_type(b3) # N: Revealed type is "str" + +m4: tuple[int] | list[str] +match m4: + case (a4, b4): + reveal_type(a4) # N: Revealed type is "float" + reveal_type(b4) # N: Revealed type is "str" [builtins fixtures/tuple.pyi] From 853b76220a03a7a140ff318b55aeaf1f6a0de8d6 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 15:55:33 -0400 Subject: [PATCH 03/11] almost working --- mypy/checkpattern.py | 119 +++++++++++++++++----------- mypy/types.py | 9 +++ test-data/unit/check-python310.test | 49 ++++++++++-- 3 files changed, 123 insertions(+), 54 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 48840466f0d8..dc969d9c4c46 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -3,7 +3,8 @@ from __future__ import annotations from collections import defaultdict -from typing import Final, NamedTuple +from itertools import zip_longest +from typing import Final, Literal, NamedTuple from mypy import message_registry from mypy.checker_shared import TypeCheckerSharedApi, TypeRange @@ -245,34 +246,79 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: # # get inner types of original type # - unpack_index = None - if isinstance(current_type, TupleType): - inner_types = current_type.items - unpack_index = find_unpack_in_list(inner_types) - if unpack_index is None: - size_diff = len(inner_types) - required_patterns - if size_diff < 0: - return self.early_non_match() - elif size_diff > 0 and star_position is None: - return self.early_non_match() + # 1. Go through all possible types and filter to only those which are sequences that could match that number of items + # 2. If there are multiple tuples left with unpacks, then use the fallback logic where we union all items types + # 3. Otherwise, take the product of the item types so that each index can have a unique type + + # state of matching + state: ( + # Start in the state where we not encountered an unpack. + # a list of all the possible types that could match the sequence. If it's a tuple, then store one for each index + tuple[Literal["NO_UNPACK"], list[list[Type]]] | + # If we encounter a single tuple with an unpack, store the type, the unpack index, and the index in the union type + tuple[Literal["UNPACK"], TupleType, int, int] | + # If we have encountered a tuple with an unpack plus any other types, then store a list of them. For any tuples + # without unpacks, store them as a list of their items. + tuple[Literal["MULTI_UNPACK"], list[list[Type]]] + ) = ("NO_UNPACK", []) + for i, t in enumerate(current_type.items) if isinstance(current_type, UnionType) else ((0, current_type),): + t = get_proper_type(t) + n_patterns = len(o.patterns) + if isinstance(t, TupleType): + t_items = t.items + unpack_index = find_unpack_in_list(t_items) + if unpack_index is None: + size_diff = len(t_items) - required_patterns + if size_diff < 0: + continue + elif size_diff > 0 and star_position is None: + continue + inner_t = list(t_items) + else: + normalized_inner_types = [] + for it in t_items: + # Unfortunately, it is not possible to "split" the TypeVarTuple + # into individual items, so we just use its upper bound for the whole + # analysis instead. + if isinstance(it, UnpackType) and isinstance(it.type, TypeVarTupleType): + it = UnpackType(it.type.upper_bound) + normalized_inner_types.append(it) + t_items = normalized_inner_types + t = t.copy_modified(items=normalized_inner_types) + if len(t_items) - 1 > required_patterns and star_position is None: + continue + if state[0] == "NO_UNPACK" and not state[1]: + state = ("UNPACK", t, unpack_index, i) + continue + inner_t = [self.chk.iterable_item_type(tuple_fallback(t), o)] * n_patterns + elif isinstance(t, AnyType): + inner_t = [AnyType(TypeOfAny.from_another_any, t)] * n_patterns + elif self.chk.type_is_iterable(t) and isinstance(t, Instance): + inner_t = [self.chk.iterable_item_type(t, o)] * n_patterns else: - normalized_inner_types = [] - for it in inner_types: - # Unfortunately, it is not possible to "split" the TypeVarTuple - # into individual items, so we just use its upper bound for the whole - # analysis instead. - if isinstance(it, UnpackType) and isinstance(it.type, TypeVarTupleType): - it = UnpackType(it.type.upper_bound) - normalized_inner_types.append(it) - inner_types = normalized_inner_types - current_type = current_type.copy_modified(items=normalized_inner_types) - if len(inner_types) - 1 > required_patterns and star_position is None: - return self.early_non_match() + continue + # if we previously encountered an unpack, then change the state. + if state[0] == "UNPACK": + # if we already unpacked something, change this + state = ("MULTI_UNPACK", [[self.chk.iterable_item_type(tuple_fallback(state[1]), o)] * n_patterns]) + assert state[0] != "UNPACK" # for type checker + state[1].append(inner_t) + + if state[0] == "UNPACK": + _, update_tuple_type, unpack_index, union_index = state + inner_types = update_tuple_type.items + if isinstance(current_type, UnionType): + union_items = list(current_type.items) + union_items[union_index] = update_tuple_type + current_type = current_type.copy_modified(items=union_items) + else: + assert unpack_index == 0, "Unpack index should be 0 for non-union types" + current_type = update_tuple_type else: - inner_type = self.get_sequence_type(current_type, o) - if inner_type is None: - inner_type = self.chk.named_type("builtins.object") - inner_types = [inner_type] * len(o.patterns) + unpack_index = None + if not state[1]: + return self.early_non_match() + inner_types = [make_simplified_union(x) for x in zip_longest(*state[1], fillvalue=UninhabitedType())] # # match inner patterns @@ -351,25 +397,6 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: new_type = self.narrow_sequence_child(current_type, new_inner_type, o) return PatternType(new_type, rest_type, captures) - def get_sequence_type(self, t: Type, context: Context) -> Type | None: - t = get_proper_type(t) - if isinstance(t, AnyType): - return AnyType(TypeOfAny.from_another_any, t) - if isinstance(t, UnionType): - items = [self.get_sequence_type(item, context) for item in t.items] - not_none_items = [item for item in items if item is not None] - if not_none_items: - return make_simplified_union(not_none_items) - else: - return None - - if self.chk.type_is_iterable(t) and isinstance(t, (Instance, TupleType)): - if isinstance(t, TupleType): - t = tuple_fallback(t) - return self.chk.iterable_item_type(t, context) - else: - return None - def contract_starred_pattern_types( self, types: list[Type], star_pos: int | None, num_patterns: int ) -> list[Type]: diff --git a/mypy/types.py b/mypy/types.py index b4771b15f77a..5f25285404aa 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2918,6 +2918,15 @@ def __init__( self.original_str_expr: str | None = None self.original_str_fallback: str | None = None + def copy_modified(self, *, items: Sequence[Type]) -> UnionType: + return UnionType( + items, + line=self.line, + column=self.column, + is_evaluated=self.is_evaluated, + uses_pep604_syntax=self.uses_pep604_syntax, + ) + def can_be_true_default(self) -> bool: return any(item.can_be_true for item in self.items) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 49889a92cc12..661589240408 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1560,29 +1560,62 @@ match m6: [builtins fixtures/tuple.pyi] [case testMatchTupleUnions] +from typing_extensions import Unpack + m1: tuple[int, str] | None match m1: case (a1, b1): - reveal_type(a1) # N: Revealed type is "int" - reveal_type(b1) # N: Revealed type is "str" + reveal_type(a1) # N: Revealed type is "builtins.int" + reveal_type(b1) # N: Revealed type is "builtins.str" m2: tuple[int, str] | tuple[float, str] match m2: case (a2, b2): - reveal_type(a2) # N: Revealed type is "Union[int, float]" - reveal_type(b2) # N: Revealed type is "str" + reveal_type(a2) # N: Revealed type is "Union[builtins.int, builtins.float]" + reveal_type(b2) # N: Revealed type is "builtins.str" m3: tuple[int] | tuple[float, str] match m3: case (a3, b3): - reveal_type(a3) # N: Revealed type is "float" - reveal_type(b3) # N: Revealed type is "str" + reveal_type(a3) # N: Revealed type is "builtins.float" + reveal_type(b3) # N: Revealed type is "builtins.str" m4: tuple[int] | list[str] match m4: case (a4, b4): - reveal_type(a4) # N: Revealed type is "float" - reveal_type(b4) # N: Revealed type is "str" + reveal_type(a4) # N: Revealed type is "builtins.str" + reveal_type(b4) # N: Revealed type is "builtins.str" + +# properly handles unpack when all other patterns are not sequences +m5: tuple[int, Unpack[tuple[float, ...]]] | None +match m5: + case (a5, b5): + reveal_type(a5) # N: Revealed type is "builtins.int" + reveal_type(b5) # N: Revealed type is "builtins.float" + +# currently can't handle combing unpacking with other sequence patterns, if this happens revert to worst case +# of combing all types +m6: tuple[int, Unpack[tuple[float, ...]]] | list[str] +match m6: + case (a6, b6): + reveal_type(a6) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" + reveal_type(b6) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" + +# but do still seperate types from non unpacked types +m7: tuple[int, Unpack[tuple[float, ...]]] | tuple[str, bool] +match m7: + case (a7, b7, *rest7): + reveal_type(a7) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" + reveal_type(b7) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.bool]" + reveal_type(rest7) # N: Revealed type is "builtins.list[Union[builtins.int, builtins.float]]" + +# verify that if we are unpacking, it will get the type of the sequence if the tuple is too short +m8: tuple[int, str] | list[float] +match m8: + case (a8, b8, *rest8): + reveal_type(a8) # N: Revealed type is "Union[builtins.int, builtins.float]" + reveal_type(b8) # N: Revealed type is "Union[builtins.str, builtins.float]" + reveal_type(rest8) # N: Revealed type is "builtins.list[builtins.float]" [builtins fixtures/tuple.pyi] From 2e0ce8027fda5b4e5f271880628694084f1a7d3e Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 16:02:16 -0400 Subject: [PATCH 04/11] Fi test --- mypy/checkpattern.py | 6 +++--- test-data/unit/check-python310.test | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index dc969d9c4c46..89fae8ae4611 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -247,8 +247,9 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: # get inner types of original type # # 1. Go through all possible types and filter to only those which are sequences that could match that number of items - # 2. If there are multiple tuples left with unpacks, then use the fallback logic where we union all items types - # 3. Otherwise, take the product of the item types so that each index can have a unique type + # 2. If there is exactly one tuple left with an unpack, then use that type and the unpack index + # 3. Otherwise, take the product of the item types so that each index can have a unique type. For tuples with unpack + # fallback to merging all of their types for each index since we can't handle multiple unpacked items at once yet. # state of matching state: ( @@ -303,7 +304,6 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: state = ("MULTI_UNPACK", [[self.chk.iterable_item_type(tuple_fallback(state[1]), o)] * n_patterns]) assert state[0] != "UNPACK" # for type checker state[1].append(inner_t) - if state[0] == "UNPACK": _, update_tuple_type, unpack_index, union_index = state inner_types = update_tuple_type.items diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 661589240408..06ab191e01d6 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1602,11 +1602,11 @@ match m6: reveal_type(b6) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" # but do still seperate types from non unpacked types -m7: tuple[int, Unpack[tuple[float, ...]]] | tuple[str, bool] +m7: tuple[int, Unpack[tuple[float, ...]]] | tuple[str, str] match m7: case (a7, b7, *rest7): reveal_type(a7) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" - reveal_type(b7) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.bool]" + reveal_type(b7) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" reveal_type(rest7) # N: Revealed type is "builtins.list[Union[builtins.int, builtins.float]]" # verify that if we are unpacking, it will get the type of the sequence if the tuple is too short From 13fc8a691b7eb27fc4edbc3d57c2672ac4d31665 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 16:04:08 -0400 Subject: [PATCH 05/11] Format --- mypy/checkpattern.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 89fae8ae4611..d620911b4b64 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -255,14 +255,20 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: state: ( # Start in the state where we not encountered an unpack. # a list of all the possible types that could match the sequence. If it's a tuple, then store one for each index - tuple[Literal["NO_UNPACK"], list[list[Type]]] | + tuple[Literal["NO_UNPACK"], list[list[Type]]] + | # If we encounter a single tuple with an unpack, store the type, the unpack index, and the index in the union type - tuple[Literal["UNPACK"], TupleType, int, int] | + tuple[Literal["UNPACK"], TupleType, int, int] + | # If we have encountered a tuple with an unpack plus any other types, then store a list of them. For any tuples # without unpacks, store them as a list of their items. tuple[Literal["MULTI_UNPACK"], list[list[Type]]] - ) = ("NO_UNPACK", []) - for i, t in enumerate(current_type.items) if isinstance(current_type, UnionType) else ((0, current_type),): + ) = ("NO_UNPACK", []) + for i, t in ( + enumerate(current_type.items) + if isinstance(current_type, UnionType) + else ((0, current_type),) + ): t = get_proper_type(t) n_patterns = len(o.patterns) if isinstance(t, TupleType): @@ -301,8 +307,11 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: # if we previously encountered an unpack, then change the state. if state[0] == "UNPACK": # if we already unpacked something, change this - state = ("MULTI_UNPACK", [[self.chk.iterable_item_type(tuple_fallback(state[1]), o)] * n_patterns]) - assert state[0] != "UNPACK" # for type checker + state = ( + "MULTI_UNPACK", + [[self.chk.iterable_item_type(tuple_fallback(state[1]), o)] * n_patterns], + ) + assert state[0] != "UNPACK" # for type checker state[1].append(inner_t) if state[0] == "UNPACK": _, update_tuple_type, unpack_index, union_index = state @@ -318,7 +327,10 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: unpack_index = None if not state[1]: return self.early_non_match() - inner_types = [make_simplified_union(x) for x in zip_longest(*state[1], fillvalue=UninhabitedType())] + inner_types = [ + make_simplified_union(x) + for x in zip_longest(*state[1], fillvalue=UninhabitedType()) + ] # # match inner patterns From 543d224b2c3a0f8d008f7f37e61f2fdda911c4a6 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 16:39:43 -0400 Subject: [PATCH 06/11] Fix tests by handling object properly --- mypy/checkpattern.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index d620911b4b64..f25c4c4cda38 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -252,6 +252,8 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: # fallback to merging all of their types for each index since we can't handle multiple unpacked items at once yet. # state of matching + # whether one of the possible types is not handled, in which case we want to return an object + unknown_value = False state: ( # Start in the state where we not encountered an unpack. # a list of all the possible types that could match the sequence. If it's a tuple, then store one for each index @@ -303,6 +305,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: elif self.chk.type_is_iterable(t) and isinstance(t, Instance): inner_t = [self.chk.iterable_item_type(t, o)] * n_patterns else: + unknown_value = True continue # if we previously encountered an unpack, then change the state. if state[0] == "UNPACK": @@ -321,16 +324,19 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: union_items[union_index] = update_tuple_type current_type = current_type.copy_modified(items=union_items) else: - assert unpack_index == 0, "Unpack index should be 0 for non-union types" + assert union_index == 0, "Unpack index should be 0 for non-union types" current_type = update_tuple_type else: unpack_index = None - if not state[1]: + if state[1]: + inner_types = [ + make_simplified_union(x) + for x in zip_longest(*state[1], fillvalue=UninhabitedType()) + ] + elif unknown_value: + inner_types = [self.chk.named_type("builtins.object")] * n_patterns + else: return self.early_non_match() - inner_types = [ - make_simplified_union(x) - for x in zip_longest(*state[1], fillvalue=UninhabitedType()) - ] # # match inner patterns From c9d123319d48bd76f4e436779777994c4480bbc8 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Wed, 6 Aug 2025 22:04:24 -0400 Subject: [PATCH 07/11] Simplify logic --- mypy/checkpattern.py | 90 +++++++++++------------------ test-data/unit/check-python310.test | 4 +- 2 files changed, 36 insertions(+), 58 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index f25c4c4cda38..d0463af060c5 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -3,8 +3,7 @@ from __future__ import annotations from collections import defaultdict -from itertools import zip_longest -from typing import Final, Literal, NamedTuple +from typing import Final, NamedTuple from mypy import message_registry from mypy.checker_shared import TypeCheckerSharedApi, TypeRange @@ -245,36 +244,25 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: # # get inner types of original type - # # 1. Go through all possible types and filter to only those which are sequences that could match that number of items # 2. If there is exactly one tuple left with an unpack, then use that type and the unpack index # 3. Otherwise, take the product of the item types so that each index can have a unique type. For tuples with unpack # fallback to merging all of their types for each index since we can't handle multiple unpacked items at once yet. - # state of matching - # whether one of the possible types is not handled, in which case we want to return an object - unknown_value = False - state: ( - # Start in the state where we not encountered an unpack. - # a list of all the possible types that could match the sequence. If it's a tuple, then store one for each index - tuple[Literal["NO_UNPACK"], list[list[Type]]] - | - # If we encounter a single tuple with an unpack, store the type, the unpack index, and the index in the union type - tuple[Literal["UNPACK"], TupleType, int, int] - | - # If we have encountered a tuple with an unpack plus any other types, then store a list of them. For any tuples - # without unpacks, store them as a list of their items. - tuple[Literal["MULTI_UNPACK"], list[list[Type]]] - ) = ("NO_UNPACK", []) - for i, t in ( - enumerate(current_type.items) - if isinstance(current_type, UnionType) - else ((0, current_type),) + # Whether we have encountered a type that we don't know how to handle in the union + unknown_type = False + # A list of types that could match any of the items in the sequence. + sequence_types: list[Type] = [] + # A list of tuple types that could match the sequence, per index + tuple_types: list[list[Type]] = [] + # A list of all the unpack tuple types that we encountered, each containing the tuple type, unpack index, and union index + unpack_tuple_types: list[tuple[TupleType, int, int]] = [] + for i, t in enumerate( + current_type.items if isinstance(current_type, UnionType) else [current_type] ): t = get_proper_type(t) - n_patterns = len(o.patterns) if isinstance(t, TupleType): - t_items = t.items + t_items = list(t.items) unpack_index = find_unpack_in_list(t_items) if unpack_index is None: size_diff = len(t_items) - required_patterns @@ -282,7 +270,9 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: continue elif size_diff > 0 and star_position is None: continue - inner_t = list(t_items) + elif not size_diff and star_position is not None: + t_items.append(UninhabitedType()) # add additional item for star if its empty + tuple_types.append(t_items) else: normalized_inner_types = [] for it in t_items: @@ -296,47 +286,35 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: t = t.copy_modified(items=normalized_inner_types) if len(t_items) - 1 > required_patterns and star_position is None: continue - if state[0] == "NO_UNPACK" and not state[1]: - state = ("UNPACK", t, unpack_index, i) - continue - inner_t = [self.chk.iterable_item_type(tuple_fallback(t), o)] * n_patterns + unpack_tuple_types.append((t, unpack_index, i)) + # add the combined tuple type to the sequence types in case we have multiple unpacks we want to combine them all + sequence_types.append(self.chk.iterable_item_type(tuple_fallback(t), o)) elif isinstance(t, AnyType): - inner_t = [AnyType(TypeOfAny.from_another_any, t)] * n_patterns + sequence_types.append(AnyType(TypeOfAny.from_another_any, t)) elif self.chk.type_is_iterable(t) and isinstance(t, Instance): - inner_t = [self.chk.iterable_item_type(t, o)] * n_patterns + sequence_types.append(self.chk.iterable_item_type(t, o)) else: - unknown_value = True - continue - # if we previously encountered an unpack, then change the state. - if state[0] == "UNPACK": - # if we already unpacked something, change this - state = ( - "MULTI_UNPACK", - [[self.chk.iterable_item_type(tuple_fallback(state[1]), o)] * n_patterns], - ) - assert state[0] != "UNPACK" # for type checker - state[1].append(inner_t) - if state[0] == "UNPACK": - _, update_tuple_type, unpack_index, union_index = state - inner_types = update_tuple_type.items + unknown_type = True + # if we only got one unpack tuple type, we can use that + unpack_index = None + if len(unpack_tuple_types) == 1 and len(sequence_types) == 1 and not tuple_types: + update_tuple_type, unpack_index, union_index = unpack_tuple_types[0] + inner_types: list[Type] = update_tuple_type.items if isinstance(current_type, UnionType): union_items = list(current_type.items) union_items[union_index] = update_tuple_type current_type = current_type.copy_modified(items=union_items) else: - assert union_index == 0, "Unpack index should be 0 for non-union types" current_type = update_tuple_type + # if we only got tuples we can't match, then exit early + elif not tuple_types and not sequence_types and not unknown_type: + return self.early_non_match() + elif tuple_types: + inner_types = [make_simplified_union([*sequence_types, *x]) for x in zip(*tuple_types)] else: - unpack_index = None - if state[1]: - inner_types = [ - make_simplified_union(x) - for x in zip_longest(*state[1], fillvalue=UninhabitedType()) - ] - elif unknown_value: - inner_types = [self.chk.named_type("builtins.object")] * n_patterns - else: - return self.early_non_match() + object_type = self.chk.named_type("builtins.object") + inner_type = make_simplified_union(sequence_types) if sequence_types else object_type + inner_types = [inner_type] * len(o.patterns) # # match inner patterns diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 06ab191e01d6..6661e3a4a74d 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1613,8 +1613,8 @@ match m7: m8: tuple[int, str] | list[float] match m8: case (a8, b8, *rest8): - reveal_type(a8) # N: Revealed type is "Union[builtins.int, builtins.float]" - reveal_type(b8) # N: Revealed type is "Union[builtins.str, builtins.float]" + reveal_type(a8) # N: Revealed type is "Union[builtins.float, builtins.int]" + reveal_type(b8) # N: Revealed type is "Union[builtins.float, builtins.str]" reveal_type(rest8) # N: Revealed type is "builtins.list[builtins.float]" [builtins fixtures/tuple.pyi] From 9ca630ce493e02e4a603fc2a3ac0ee9414230873 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:05:49 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/checkpattern.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index d0463af060c5..4ae4fce01105 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -271,7 +271,9 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: elif size_diff > 0 and star_position is None: continue elif not size_diff and star_position is not None: - t_items.append(UninhabitedType()) # add additional item for star if its empty + t_items.append( + UninhabitedType() + ) # add additional item for star if its empty tuple_types.append(t_items) else: normalized_inner_types = [] From ebc59ef0bd0b3ff381b385bded671cf3dc635ea3 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Thu, 7 Aug 2025 07:39:04 -0400 Subject: [PATCH 09/11] fix mypy error from overlapping variable --- mypy/checkpattern.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 4ae4fce01105..e4715a7701ab 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -315,8 +315,8 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: inner_types = [make_simplified_union([*sequence_types, *x]) for x in zip(*tuple_types)] else: object_type = self.chk.named_type("builtins.object") - inner_type = make_simplified_union(sequence_types) if sequence_types else object_type - inner_types = [inner_type] * len(o.patterns) + unioned = make_simplified_union(sequence_types) if sequence_types else object_type + inner_types = [unioned] * len(o.patterns) # # match inner patterns From 64a6f03f90de8e669b3ce0ea68a328e96945ffac Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Thu, 7 Aug 2025 07:39:52 -0400 Subject: [PATCH 10/11] black --- mypy/checkpattern.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index e4715a7701ab..8f38807168b0 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -271,9 +271,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: elif size_diff > 0 and star_position is None: continue elif not size_diff and star_position is not None: - t_items.append( - UninhabitedType() - ) # add additional item for star if its empty + t_items.append(UninhabitedType()) tuple_types.append(t_items) else: normalized_inner_types = [] From 5a6464afb8a3ae59d753969251a34f7f39fee6af Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Thu, 7 Aug 2025 07:44:24 -0400 Subject: [PATCH 11/11] fix spelling error --- test-data/unit/check-python310.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 6661e3a4a74d..dfa2d16e375c 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1601,7 +1601,7 @@ match m6: reveal_type(a6) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" reveal_type(b6) # N: Revealed type is "Union[builtins.int, builtins.float, builtins.str]" -# but do still seperate types from non unpacked types +# but do still separate types from non unpacked types m7: tuple[int, Unpack[tuple[float, ...]]] | tuple[str, str] match m7: case (a7, b7, *rest7):