From 1b524a56023bba8ecf282382de8a2fff90a60db7 Mon Sep 17 00:00:00 2001 From: KirschQAQ <114209152+KirschBluteX@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:46:23 -0700 Subject: [PATCH 1/2] Fix variadic ParamSpec expansion crash Expand unpacked TypeVarTuple parameters while preserving aligned argument types, kinds, and names. Fixes #21778. --- mypy/expandtype.py | 24 ++++++++++++++++- .../unit/check-parameter-specification.test | 26 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index fd507216a6be9..fbd559bbd0f04 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -400,7 +400,29 @@ def expand_unpack(self, t: UnpackType) -> list[Type]: raise RuntimeError(f"Invalid type replacement to expand: {repl}") def visit_parameters(self, t: Parameters) -> Type: - return t.copy_modified(arg_types=self.expand_types(t.arg_types)) + arg_types: list[Type] = [] + arg_kinds: list[ArgKind] = [] + arg_names: list[str | None] = [] + for arg_type, arg_kind, arg_name in zip(t.arg_types, t.arg_kinds, t.arg_names): + if ( + arg_kind == ARG_STAR + and isinstance(arg_type, UnpackType) + and isinstance(arg_type.type, TypeVarTupleType) + ): + expanded = self.expand_unpack(arg_type) + for item in expanded: + arg_types.append(item) + if isinstance(item, UnpackType): + arg_kinds.append(ARG_STAR) + arg_names.append(arg_name) + else: + arg_kinds.append(ArgKind.ARG_POS) + arg_names.append(None) + else: + arg_types.append(arg_type.accept(self)) + arg_kinds.append(arg_kind) + arg_names.append(arg_name) + return t.copy_modified(arg_types=arg_types, arg_kinds=arg_kinds, arg_names=arg_names) def interpolate_args_for_unpack(self, t: CallableType, var_arg: UnpackType) -> list[Type]: star_index = t.arg_kinds.index(ARG_STAR) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index d1e928441a9ec..d32c2401c6ad6 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -141,6 +141,32 @@ reveal_type(whatever) # N: Revealed type is "def (x: builtins.int) -> builtins. reveal_type(whatever(217)) # N: Revealed type is "builtins.list[builtins.int]" [builtins fixtures/paramspec.pyi] +[case testParamSpecVariadicContextManager] +from typing import Callable, Generic, TypeVar, TypeVarTuple, Unpack +from typing_extensions import ParamSpec + +P = ParamSpec("P") +R = TypeVar("R") +Ts = TypeVarTuple("Ts") + +class contextmanager(Generic[P, R]): + def __init__(self, func: Callable[P, R]) -> None: ... + + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> "_contextmanager_cls[P, R]": ... + +class _contextmanager_cls(Generic[P, R]): + def __enter__(self) -> R: ... + def __exit__(self, *args: object) -> bool: ... + +@contextmanager +def print_args(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: ... + +with print_args(2, "x") as value: + reveal_type(value) # N: Revealed type is "tuple[builtins.int, builtins.str]" + +reveal_type(print_args(2, "x")) # N: Revealed type is "__main__._contextmanager_cls[[Literal[2]?, Literal['x']?], tuple[Literal[2]?, Literal['x']?]]" +[builtins fixtures/tuple.pyi] + [case testInvalidParamSpecType] from typing import ParamSpec From 088cc2daa4b531ead49f77441b700f32cd2ea343 Mon Sep 17 00:00:00 2001 From: KirschQAQ <114209152+KirschBluteX@users.noreply.github.com> Date: Sat, 15 Aug 2026 16:31:44 -0700 Subject: [PATCH 2/2] Preserve nested variadic ParamSpec arguments Keep a residual unpack and its suffix together as one vararg when expanding Parameters. This avoids positional arguments after *args and preserves nested TypeVarTuple call semantics. --- mypy/expandtype.py | 31 ++++++++++++++----- .../unit/check-parameter-specification.test | 6 ++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/mypy/expandtype.py b/mypy/expandtype.py index fbd559bbd0f04..c9d233a0a08b2 100644 --- a/mypy/expandtype.py +++ b/mypy/expandtype.py @@ -410,14 +410,29 @@ def visit_parameters(self, t: Parameters) -> Type: and isinstance(arg_type.type, TypeVarTupleType) ): expanded = self.expand_unpack(arg_type) - for item in expanded: - arg_types.append(item) - if isinstance(item, UnpackType): - arg_kinds.append(ARG_STAR) - arg_names.append(arg_name) - else: - arg_kinds.append(ArgKind.ARG_POS) - arg_names.append(None) + # Keep a residual unpack and its suffix together as one vararg. Otherwise + # the suffix would become positional arguments placed after *args. + unpack_index = next( + (i for i, item in enumerate(expanded) if isinstance(item, UnpackType)), None + ) + if unpack_index is not None: + arg_types.extend(expanded[:unpack_index]) + arg_kinds.extend([ArgKind.ARG_POS] * unpack_index) + arg_names.extend([None] * unpack_index) + + unpack = expanded[unpack_index] + assert isinstance(unpack, UnpackType) + if unpack_index < len(expanded) - 1: + unpack = UnpackType( + TupleType(expanded[unpack_index:], arg_type.type.tuple_fallback) + ) + arg_types.append(unpack) + arg_kinds.append(ARG_STAR) + arg_names.append(arg_name) + else: + arg_types.extend(expanded) + arg_kinds.extend([ArgKind.ARG_POS] * len(expanded)) + arg_names.extend([None] * len(expanded)) else: arg_types.append(arg_type.accept(self)) arg_kinds.append(arg_kind) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index d32c2401c6ad6..ef35ae23fbf25 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -148,6 +148,7 @@ from typing_extensions import ParamSpec P = ParamSpec("P") R = TypeVar("R") Ts = TypeVarTuple("Ts") +Us = TypeVarTuple("Us") class contextmanager(Generic[P, R]): def __init__(self, func: Callable[P, R]) -> None: ... @@ -157,6 +158,7 @@ class contextmanager(Generic[P, R]): class _contextmanager_cls(Generic[P, R]): def __enter__(self) -> R: ... def __exit__(self, *args: object) -> bool: ... + def invoke(self, *args: P.args, **kwargs: P.kwargs) -> R: ... @contextmanager def print_args(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: ... @@ -165,6 +167,10 @@ with print_args(2, "x") as value: reveal_type(value) # N: Revealed type is "tuple[builtins.int, builtins.str]" reveal_type(print_args(2, "x")) # N: Revealed type is "__main__._contextmanager_cls[[Literal[2]?, Literal['x']?], tuple[Literal[2]?, Literal['x']?]]" + +def forward(*args: Unpack[Us]) -> None: + manager = print_args(0, *args, "end") + reveal_type(manager.invoke(0, *args, "end")) # N: Revealed type is "tuple[builtins.int, Unpack[Us`-1], builtins.str]" [builtins fixtures/tuple.pyi] [case testInvalidParamSpecType]