From a9c40b891b5fbbbae6619088c4eb98f4d5019e83 Mon Sep 17 00:00:00 2001 From: Gene Parmesan Thomas <201852096+gopoto@users.noreply.github.com> Date: Tue, 11 Mar 2025 22:45:14 -0700 Subject: [PATCH 1/2] fix: Narrowing with "tags" on unions (of TypedDicts or normal classes) doesn't work with the match statement Signed-off-by: Gene Parmesan Thomas <201852096+gopoto@users.noreply.github.com> --- CHANGELOG.md | 8 +++++ mypy/checker.py | 8 +++++ test-data/unit/check-python310.test | 48 +++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc87cae5065b..6f0bb1cd6f43c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,14 @@ class A: Contributed by Marc Mueller (PR [18641](https://github.com/python/mypy/pull/18641)) +### Other Notable Fixes and Improvements + +* Pattern matching on discriminant attributes and keys in union types + (such as `tag` fields in `TypedDict`s or classes) now correctly narrows + the subject type within each `case` block. Previously, mypy would fail + to narrow the parent type when matching on expressions like `match d["tag"]` + or `match d.tag`. This fixes issue [#16286](https://github.com/python/mypy/issues/16286). + ## Mypy 1.15 We’ve just uploaded mypy 1.15 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). diff --git a/mypy/checker.py b/mypy/checker.py index 6d7e8fa215a19..3c5ddf8c40d64 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5527,6 +5527,14 @@ def visit_match_stmt(self, s: MatchStmt) -> None: pattern_map, else_map = conditional_types_to_typemaps( named_subject, pattern_type.type, pattern_type.rest_type ) + # Also refine the parent expression of the subject. + # For example, if the subject is an index or attribute expression like + # ``d["key"]`` or ``d.attr``, propagate any narrowing information about + # the subject back up to ``d`` (and recursively, to further parents). + # This mirrors the behavior of our conditional (``if``) binder, + # which calls ``propagate_up_typemap_info`` when handling comparisons. + pattern_map = self.propagate_up_typemap_info(pattern_map) + else_map = self.propagate_up_typemap_info(else_map) self.remove_capture_conflicts(pattern_type.captures, inferred_types) self.push_type_map(pattern_map, from_assignment=False) if pattern_map: diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 016f50552a5f5..18554a3540e64 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -332,6 +332,54 @@ match [SubClass("a"), SubClass("b")]: reveal_type(rest) # N: Revealed type is "builtins.list[__main__.Example]" [builtins fixtures/tuple.pyi] +# Narrowing union-based values via a literal pattern on an indexed/attribute subject +# ------------------------------------------------------------------------------- +# Literal patterns against a union of types can be used to narrow the subject +# itself, not just the expression being matched. Previously, the patterns below +# failed to narrow the `d` variable, leading to errors for missing members; we +# now propagate the type information up to the parent. + +[case testMatchNarrowingUnionTypedDictViaIndex] +from typing import Literal, TypedDict + +class A(TypedDict): + tag: Literal["a"] + name: str + +class B(TypedDict): + tag: Literal["b"] + num: int + +d: A | B +match d["tag"]: + case "a": + reveal_type(d) # N: Revealed type is "TypedDict('__main__.A', {'tag': Literal['a'], 'name': builtins.str})" + reveal_type(d["name"]) # N: Revealed type is "builtins.str" + case "b": + reveal_type(d) # N: Revealed type is "TypedDict('__main__.B', {'tag': Literal['b'], 'num': builtins.int})" + reveal_type(d["num"]) # N: Revealed type is "builtins.int" +[typing fixtures/typing-typeddict.pyi] + +[case testMatchNarrowingUnionClassViaAttribute] +from typing import Literal + +class A: + tag: Literal["a"] + name: str + +class B: + tag: Literal["b"] + num: int + +d: A | B +match d.tag: + case "a": + reveal_type(d) # N: Revealed type is "__main__.A" + reveal_type(d.name) # N: Revealed type is "builtins.str" + case "b": + reveal_type(d) # N: Revealed type is "__main__.B" + reveal_type(d.num) # N: Revealed type is "builtins.int" + [case testMatchSequenceUnion-skip] from typing import List, Union m: Union[List[List[str]], str] From 0a6333a9d41939968a946c9f96197877b8cdba61 Mon Sep 17 00:00:00 2001 From: gopoto <201852096+gopoto@users.noreply.github.com> Date: Fri, 14 Mar 2025 00:44:04 +0000 Subject: [PATCH 2/2] Address review: drop changelog entry, remove comment Signed-off-by: gopoto <201852096+gopoto@users.noreply.github.com> --- CHANGELOG.md | 8 -------- mypy/checker.py | 6 ------ 2 files changed, 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0bb1cd6f43c..5cc87cae5065b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,14 +45,6 @@ class A: Contributed by Marc Mueller (PR [18641](https://github.com/python/mypy/pull/18641)) -### Other Notable Fixes and Improvements - -* Pattern matching on discriminant attributes and keys in union types - (such as `tag` fields in `TypedDict`s or classes) now correctly narrows - the subject type within each `case` block. Previously, mypy would fail - to narrow the parent type when matching on expressions like `match d["tag"]` - or `match d.tag`. This fixes issue [#16286](https://github.com/python/mypy/issues/16286). - ## Mypy 1.15 We’ve just uploaded mypy 1.15 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)). diff --git a/mypy/checker.py b/mypy/checker.py index 3c5ddf8c40d64..c9e0dcec6bd06 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5527,12 +5527,6 @@ def visit_match_stmt(self, s: MatchStmt) -> None: pattern_map, else_map = conditional_types_to_typemaps( named_subject, pattern_type.type, pattern_type.rest_type ) - # Also refine the parent expression of the subject. - # For example, if the subject is an index or attribute expression like - # ``d["key"]`` or ``d.attr``, propagate any narrowing information about - # the subject back up to ``d`` (and recursively, to further parents). - # This mirrors the behavior of our conditional (``if``) binder, - # which calls ``propagate_up_typemap_info`` when handling comparisons. pattern_map = self.propagate_up_typemap_info(pattern_map) else_map = self.propagate_up_typemap_info(else_map) self.remove_capture_conflicts(pattern_type.captures, inferred_types)