From 0a7b9200a32a6515f7e93cbcaeac103bb7f7a402 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 2 Oct 2025 23:51:59 +0200 Subject: [PATCH 1/2] Fix reachability in nested match sequence patterns --- mypy/checkpattern.py | 16 +++++++++++++++- test-data/unit/check-python310.test | 18 +++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index f81684d2f44a..9d4c00b6c47b 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -313,7 +313,21 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: ) ) narrowed_inner_types.append(narrowed_inner_type) - inner_rest_types.append(inner_rest_type) + narrowed_ptype = get_proper_type(narrowed_inner_type) + if ( + is_uninhabited(inner_rest_type) + and isinstance(narrowed_ptype, Instance) + and ( + narrowed_ptype.type.fullname == "builtins.dict" + or narrowed_ptype.type.fullname == "builtins.list" + ) + ): + # Can't narrow rest type to uninhabited + # if narrowed_type is dict or list. + # Those can be matched by Mapping or Sequence patterns. + inner_rest_types.append(narrowed_inner_type) + else: + inner_rest_types.append(inner_rest_type) if all(not is_uninhabited(typ) for typ in narrowed_inner_types): new_type = TupleType(narrowed_inner_types, current_type.partial_fallback) else: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 7d76c09b6151..b08560103dea 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1525,6 +1525,7 @@ def f(value: Literal[1] | Literal[2]) -> int: [typing fixtures/typing-medium.pyi] [case testMatchSequencePatternNegativeNarrowing] +# flags: --warn-unreachable from typing import Literal, Union, Sequence, Tuple m1: Sequence[int | str] @@ -1577,7 +1578,22 @@ match m6: case _: reveal_type(m6) # N: Revealed type is "tuple[Union[Literal[1], Literal[2]], Union[Literal['a'], Literal['b']]]" -[builtins fixtures/tuple.pyi] +m7: dict[str, str] + +match (m7, m7): + case ({"a": "1"}, _): + reveal_type(m7) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + case (_, {"a": "2"}): + reveal_type(m7) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + +m8: list[int] + +match (m8, m8): + case ([1], _): + reveal_type(m8) # N: Revealed type is "builtins.list[builtins.int]" + case (_, [2]): + reveal_type(m8) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/dict.pyi] [case testMatchEnumSingleChoice] from enum import Enum From f6c9aa17d7b9d9fc4082989844270ce3b795b15f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 18 Oct 2025 13:27:09 +0200 Subject: [PATCH 2/2] Track node context --- mypy/checkpattern.py | 30 +++++++++++++---------------- test-data/unit/check-python310.test | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 3c4d4657e8cb..215a6dce6098 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -14,7 +14,7 @@ from mypy.maptype import map_instance_to_supertype from mypy.meet import narrow_declared_type from mypy.messages import MessageBuilder -from mypy.nodes import ARG_POS, Context, Expression, NameExpr, TypeAlias, Var +from mypy.nodes import ARG_POS, Context, Expression, NameExpr, Node, TypeAlias, Var from mypy.options import Options from mypy.patterns import ( AsPattern, @@ -104,6 +104,8 @@ class PatternChecker(PatternVisitor[PatternType]): subject_type: Type # Type of the subject to check the (sub)pattern against type_context: list[Type] + # Pattern node currently being processed + node_context: list[Node] # Types that match against self instead of their __match_args__ if used as a class pattern # Filled in from self_match_type_names self_match_types: list[Type] @@ -121,6 +123,7 @@ def __init__( self.plugin = plugin self.type_context = [] + self.node_context = [] self.self_match_types = self.generate_types_from_names(self_match_type_names) self.non_sequence_match_types = self.generate_types_from_names( non_sequence_match_type_names @@ -129,8 +132,10 @@ def __init__( def accept(self, o: Pattern, type_context: Type) -> PatternType: self.type_context.append(type_context) + self.node_context.append(o) result = o.accept(self) self.type_context.pop() + self.node_context.pop() return result @@ -140,7 +145,12 @@ def visit_as_pattern(self, o: AsPattern) -> PatternType: pattern_type = self.accept(o.pattern, current_type) typ, rest_type, type_map = pattern_type else: - typ, rest_type, type_map = current_type, UninhabitedType(), {} + typ, type_map = current_type, {} + if len(self.node_context) >= 2 and isinstance(self.node_context[-2], SequencePattern): + # Don't narrow rest type to Never if parent node is a sequence pattern + rest_type = current_type + else: + rest_type = UninhabitedType() if not is_uninhabited(typ) and o.name is not None: typ, _ = self.chk.conditional_types_with_intersection( @@ -315,21 +325,7 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: ) ) narrowed_inner_types.append(narrowed_inner_type) - narrowed_ptype = get_proper_type(narrowed_inner_type) - if ( - is_uninhabited(inner_rest_type) - and isinstance(narrowed_ptype, Instance) - and ( - narrowed_ptype.type.fullname == "builtins.dict" - or narrowed_ptype.type.fullname == "builtins.list" - ) - ): - # Can't narrow rest type to uninhabited - # if narrowed_type is dict or list. - # Those can be matched by Mapping or Sequence patterns. - inner_rest_types.append(narrowed_inner_type) - else: - inner_rest_types.append(inner_rest_type) + inner_rest_types.append(inner_rest_type) if all(not is_uninhabited(typ) for typ in narrowed_inner_types): new_type = TupleType(narrowed_inner_types, current_type.partial_fallback) else: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index ca839ab57505..052931f406f4 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1742,6 +1742,20 @@ match (m7, m7): case (_, {"a": "2"}): reveal_type(m7) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" +match (m7, m7): + case (dict(), _): + reveal_type(m7) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + case (_, dict()): + reveal_type(m7) # E: Statement is unreachable + case (_, _): + reveal_type(m7) # E: Statement is unreachable + +match (m7, 4): + case ({"a": "1"}, _): + reveal_type(m7) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + case r7: + reveal_type(r7) # N: Revealed type is "tuple[builtins.dict[builtins.str, builtins.str], Literal[4]?]" + m8: list[int] match (m8, m8): @@ -1749,6 +1763,12 @@ match (m8, m8): reveal_type(m8) # N: Revealed type is "builtins.list[builtins.int]" case (_, [2]): reveal_type(m8) # N: Revealed type is "builtins.list[builtins.int]" + +match (m8, m8): + case (list(), _): + reveal_type(m8) # N: Revealed type is "builtins.list[builtins.int]" + case (_, [2]): + reveal_type(m8) # E: Statement is unreachable [builtins fixtures/dict.pyi] [case testMatchEnumSingleChoice]