From e1ae6478fda0ee9df827cbc8a9fd2df0883c5608 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 19 Mar 2026 13:53:47 +0900 Subject: [PATCH] Allow ParamSpec in NamedTuple and TypedDict --- mypy/typeanal.py | 16 +++++++--- .../unit/check-parameter-specification.test | 32 +++++++++++++++++++ test-data/unit/fixtures/paramspec.pyi | 4 +-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index b22e1f80be592..3c36ac80aa806 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -903,8 +903,12 @@ def analyze_type_with_type_info( if info.special_alias: return instantiate_type_alias( info.special_alias, - # TODO: should we allow NamedTuples generic in ParamSpec? - self.anal_array(args, allow_unpack=True), + self.anal_array( + args, + allow_unpack=True, + allow_param_spec=True, + allow_param_spec_literals=True, + ), self.fail, False, ctx, @@ -921,8 +925,12 @@ def analyze_type_with_type_info( if info.special_alias: return instantiate_type_alias( info.special_alias, - # TODO: should we allow TypedDicts generic in ParamSpec? - self.anal_array(args, allow_unpack=True), + self.anal_array( + args, + allow_unpack=True, + allow_param_spec=True, + allow_param_spec_literals=True, + ), self.fail, False, ctx, diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b0808105a3858..6be1d7983a0ec 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2756,3 +2756,35 @@ reveal_type(Sneaky(f8, 1, y='').kwargs) # N: Revealed type is "builtins.dict[bu reveal_type(Sneaky(f9, 1, y=0).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" reveal_type(Sneaky(f9, 1, y=0, z='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})" [builtins fixtures/paramspec.pyi] + +[case testNamedTupleAndTypedDictParamSpecInteraction] +# https://github.com/python/mypy/issues/21039 +from typing import Callable, NamedTuple, TypedDict, ParamSpec, Generic + +P = ParamSpec("P") + +class X(TypedDict, Generic[P]): + c: Callable[P, None] + +def f1(y: X[P]) -> None: ... +def f2(y: X[[int]]) -> None: ... +def f3(y: X[...]) -> None: ... + +x1: X[[str]] +f1(x1) +f2(x1) # E: Argument 1 to "f2" has incompatible type "X[[str]]"; expected "X[[int]]" +f3(x1) + +class Y(NamedTuple, Generic[P]): + c: Callable[P, None] + +def g1(y: Y[P]) -> None: ... +def g2(y: Y[[int]]) -> None: ... +def g3(y: Y[...]) -> None: ... + +y1: Y[[str]] +g1(y1) +g2(y1) # E: Argument 1 to "g2" has incompatible type "Y[[str]]"; expected "Y[[int]]" +g3(y1) +[builtins fixtures/paramspec.pyi] +[typing fixtures/typing-full.pyi] diff --git a/test-data/unit/fixtures/paramspec.pyi b/test-data/unit/fixtures/paramspec.pyi index a61e5b66ae24a..af2331dbdc5d5 100644 --- a/test-data/unit/fixtures/paramspec.pyi +++ b/test-data/unit/fixtures/paramspec.pyi @@ -51,8 +51,6 @@ class tuple(Sequence[T_co], Generic[T_co]): def __len__(self) -> int: ... def count(self, obj: object) -> int: ... -class _ItemsView(Iterable[Tuple[KT, VT]]): ... - class dict(Mapping[KT, VT]): @overload def __init__(self, **kwargs: VT) -> None: ... @@ -71,7 +69,7 @@ class dict(Mapping[KT, VT]): def get(self, key: KT, default: T, /) -> Union[VT, T]: ... def __len__(self) -> int: ... def pop(self, k: KT) -> VT: ... - def items(self) -> _ItemsView[KT, VT]: ... + def items(self) -> Iterable[Tuple[KT, VT]]: ... def isinstance(x: object, t: type) -> bool: ...