From 6e84a6727d88250ec214b192736e451031c2d782 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 25 Feb 2026 01:56:45 -0800 Subject: [PATCH 1/2] Better match narrowing for irrefutable mapping patterns --- mypy/checkpattern.py | 12 ++++++++- test-data/unit/check-python310.test | 40 ++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 4fca0ce159c1..3f6d3ef2800d 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -508,12 +508,22 @@ def visit_mapping_pattern(self, o: MappingPattern) -> PatternType: captures[o.rest] = rest_type + else_type = current_type if can_match: # We can't narrow the type here, as Mapping key is invariant. new_type = self.type_context[-1] + if not o.keys: + # Match cannot be refuted, so narrow the remaining type + mapping = self.chk.named_type("typing.Mapping") + new_type, else_type = self.chk.conditional_types_with_intersection( + current_type, + [TypeRange(mapping, is_upper_bound=False)], + o, + default=current_type, + ) else: new_type = UninhabitedType() - return PatternType(new_type, current_type, captures) + return PatternType(new_type, else_type, captures) def get_mapping_item_type( self, pattern: MappingPattern, mapping_type: Type, key: Expression diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 59527bfff792..88f21dd6f401 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -620,7 +620,45 @@ match m: reveal_type(r) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" [builtins fixtures/dict.pyi] --- Mapping patterns currently do not narrow -- +[case testMatchMappingPatternNarrowing] +from typing import Mapping, Sequence + +def f1(x: dict[str, str] | list[str] | str) -> None: + match x: + case {}: + reveal_type(x) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + case [*_]: + reveal_type(x) # N: Revealed type is "builtins.list[builtins.str]" + case _: + reveal_type(x) # N: Revealed type is "builtins.str" + +def f1_rest(x: dict[str, str] | list[str] | str) -> None: + match x: + case {**rest}: + reveal_type(x) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]" + case [*_]: + reveal_type(x) # N: Revealed type is "builtins.list[builtins.str]" + case _: + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2(x: Mapping[str, str] | Sequence[str] | str) -> None: + match x: + case {}: + reveal_type(x) # N: Revealed type is "typing.Mapping[builtins.str, builtins.str]" + case [*_]: + reveal_type(x) # N: Revealed type is "typing.Sequence[builtins.str]" + case _: + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2_rest(x: Mapping[str, str] | Sequence[str] | str) -> None: + match x: + case {**rest}: + reveal_type(x) # N: Revealed type is "typing.Mapping[builtins.str, builtins.str]" + case [*_]: + reveal_type(x) # N: Revealed type is "typing.Sequence[builtins.str]" + case _: + reveal_type(x) # N: Revealed type is "builtins.str" +[builtins fixtures/dict.pyi] -- Class Pattern -- From b3706b43381db2347f6e06e11eed558e750152cd Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Thu, 26 Feb 2026 11:18:49 -0800 Subject: [PATCH 2/2] Special case Any --- mypy/checkpattern.py | 4 +++- test-data/unit/check-python310.test | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 3f6d3ef2800d..8a61453e5e72 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -515,12 +515,14 @@ def visit_mapping_pattern(self, o: MappingPattern) -> PatternType: if not o.keys: # Match cannot be refuted, so narrow the remaining type mapping = self.chk.named_type("typing.Mapping") - new_type, else_type = self.chk.conditional_types_with_intersection( + if_type, else_type = self.chk.conditional_types_with_intersection( current_type, [TypeRange(mapping, is_upper_bound=False)], o, default=current_type, ) + if not isinstance(current_type, AnyType): + new_type = if_type else: new_type = UninhabitedType() return PatternType(new_type, else_type, captures) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 88f21dd6f401..9861f0868e03 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -660,6 +660,20 @@ def f2_rest(x: Mapping[str, str] | Sequence[str] | str) -> None: reveal_type(x) # N: Revealed type is "builtins.str" [builtins fixtures/dict.pyi] +[case testMatchMappingPatternNarrowingAny] +from typing import Any + +def f1(x: Any) -> None: + match x: + case {}: + reveal_type(x) # N: Revealed type is "Any" + +def f2(x: Any) -> None: + match x: + case {"x": "y"}: + reveal_type(x) # N: Revealed type is "Any" +[builtins fixtures/dict.pyi] + -- Class Pattern -- [case testMatchClassPatternCapturePositional]