From 883b8660d1e82c7483a17c169b8c030fac924b5d Mon Sep 17 00:00:00 2001 From: Paulo Oliveira Date: Wed, 1 Apr 2026 13:53:45 -0400 Subject: [PATCH 1/3] Fix generic NamedTuple class pattern matching --- mypy/checkpattern.py | 2 ++ test-data/unit/check-python310.test | 16 ++++++++++++++++ test-data/unit/check-python312.test | 14 ++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index be6a78a021ba0..8e6612bb25a21 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -722,6 +722,8 @@ def get_class_pattern_type_ranges(self, typ: Type, o: ClassPattern) -> list[Type if isinstance(p_typ, FunctionLike) and p_typ.is_type_obj(): typ = fill_typevars_with_any(p_typ.type_object()) + if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple: + typ = typ.partial_fallback return [TypeRange(typ, is_upper_bound=False)] if ( isinstance(o.class_ref.node, Var) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index ff334ff6c692b..a5d404a662bb1 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -961,6 +961,22 @@ match m: reveal_type(m) # N: Revealed type is "__main__.A[builtins.int]" reveal_type(i) # N: Revealed type is "builtins.int" +[case testMatchClassPatternCaptureGenericNamedTupleAlreadyKnown] +from typing import Generic, NamedTuple, TypeVar + +T = TypeVar("T") + +class A(NamedTuple, Generic[T]): + a: T + +m: A[int] + +match m: + case A(a=i): + reveal_type(i) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-namedtuple.pyi] + [case testMatchClassPatternCaptureFilledGenericTypeAlias] from typing import Generic, TypeVar diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 122bf5df14e7c..24963ac2f4013 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1676,6 +1676,20 @@ e: M[bool] # E: Value of type variable "T" of "M" cannot be "bool" [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] +[case testMatchClassPatternCapturePEP695GenericNamedTupleAlreadyKnown] +from typing import NamedTuple + +class N[T](NamedTuple): + x: T + +n: N[int] + +match n: + case N(x=value): + reveal_type(value) # N: Revealed type is "builtins.int" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + [case testPEP695GenericTypedDict] from typing import TypedDict From 989729c07beffc41466d15130936eb052e50e3e1 Mon Sep 17 00:00:00 2001 From: Paulo Oliveira Date: Wed, 1 Apr 2026 18:17:18 -0400 Subject: [PATCH 2/3] Fix generic NamedTuple class pattern member lookup --- mypy/checkpattern.py | 10 +++++++--- test-data/unit/check-python310.test | 12 +++++++----- test-data/unit/check-python312.test | 12 +++++++----- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 8e6612bb25a21..fab5d64d59975 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -665,6 +665,12 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: # # Check keyword patterns # + narrowed_type_for_members = narrowed_type + if isinstance(narrowed_type_for_members, TupleType) and ( + narrowed_type_for_members.partial_fallback.type.is_named_tuple + ): + narrowed_type_for_members = narrowed_type_for_members.partial_fallback + can_match = True for keyword, pattern in keyword_pairs: key_type: Type | None = None @@ -672,7 +678,7 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: if keyword is not None: key_type = analyze_member_access( keyword, - narrowed_type, + narrowed_type_for_members, pattern, is_lvalue=False, is_super=False, @@ -722,8 +728,6 @@ def get_class_pattern_type_ranges(self, typ: Type, o: ClassPattern) -> list[Type if isinstance(p_typ, FunctionLike) and p_typ.is_type_obj(): typ = fill_typevars_with_any(p_typ.type_object()) - if isinstance(typ, TupleType) and typ.partial_fallback.type.is_named_tuple: - typ = typ.partial_fallback return [TypeRange(typ, is_upper_bound=False)] if ( isinstance(o.class_ref.node, Var) diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index a5d404a662bb1..cfd6de42e0e3e 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -962,6 +962,7 @@ match m: reveal_type(i) # N: Revealed type is "builtins.int" [case testMatchClassPatternCaptureGenericNamedTupleAlreadyKnown] +# flags: --warn-unreachable from typing import Generic, NamedTuple, TypeVar T = TypeVar("T") @@ -969,11 +970,12 @@ T = TypeVar("T") class A(NamedTuple, Generic[T]): a: T -m: A[int] - -match m: - case A(a=i): - reveal_type(i) # N: Revealed type is "builtins.int" +def f(m: A[int]) -> int: + match m: + case A(a=i): + return i + case _: + return "oops" # E: Statement is unreachable [builtins fixtures/tuple.pyi] [typing fixtures/typing-namedtuple.pyi] diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 24963ac2f4013..35dc81305a612 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1677,16 +1677,18 @@ e: M[bool] # E: Value of type variable "T" of "M" cannot be "bool" [typing fixtures/typing-full.pyi] [case testMatchClassPatternCapturePEP695GenericNamedTupleAlreadyKnown] +# flags: --warn-unreachable from typing import NamedTuple class N[T](NamedTuple): x: T -n: N[int] - -match n: - case N(x=value): - reveal_type(value) # N: Revealed type is "builtins.int" +def f(n: N[int]) -> int: + match n: + case N(x=value): + return value + case _: + return "oops" # E: Statement is unreachable [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] From e7d0b2589a884c72023af17d56124bdb3b07f321 Mon Sep 17 00:00:00 2001 From: Paulo Oliveira Date: Thu, 2 Apr 2026 11:52:42 -0400 Subject: [PATCH 3/3] Handle proper types in NamedTuple pattern member lookup --- mypy/checkpattern.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index fab5d64d59975..6661abcdbfec5 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -666,10 +666,11 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType: # Check keyword patterns # narrowed_type_for_members = narrowed_type - if isinstance(narrowed_type_for_members, TupleType) and ( - narrowed_type_for_members.partial_fallback.type.is_named_tuple + proper_narrowed_type_for_members = get_proper_type(narrowed_type_for_members) + if isinstance(proper_narrowed_type_for_members, TupleType) and ( + proper_narrowed_type_for_members.partial_fallback.type.is_named_tuple ): - narrowed_type_for_members = narrowed_type_for_members.partial_fallback + narrowed_type_for_members = proper_narrowed_type_for_members.partial_fallback can_match = True for keyword, pattern in keyword_pairs: