Proposal: Support Unpacked `TypeVarTuple` and `tuple` in `Concatenate`
まだ誰も着手していません。
- 主要言語
- Python
- スター
- 1.8k
- フォーク
- 302
- 平均マージ
- 23時間
- マージ済み PR(30日)
- 8
説明
Abstract
PEP 612 introduced ParamSpec and Concatenate to prepend fixed positional parameters to a callable's signature. PEP 646 introduced TypeVarTuple for variadic positional typing in Callable[[*Ts], R]. Because the two PEPs were developed independently, the typing specification does not allow unpacked types (*Ts or *tuple[...]) inside Concatenate.
This proposal extends Concatenate to accept unpack_expressions in its prefix, enabling Callable[Concatenate[*Ts, P], R].
Motivation
Higher-order abstractions like partial application helpers and execution wrappers need to capture an arbitrary number of leading positional arguments while preserving the remaining signature (keyword-only params, defaults, **kwargs) via ParamSpec. Today, this requires repetitive overload ladders:
from typing import Any, Callable, Concatenate, overload
class Wrapper[**P, R]:
@overload
def __init__(self, fn: Callable[P, R]) -> None: ...
@overload
def __init__[G1](
self,
fn: Callable[Concatenate[G1, P], R],
__g1: G1,
/,
) -> None: ...
@overload
def __init__[G1, G2](
self,
fn: Callable[Concatenate[G1, G2, P], R],
__g1: G1,
__g2: G2,
/,
) -> None: ...
# Must repeat up to arbitrary maximum arity...
def __init__(self, fn: Callable[..., R], *args: Any) -> None:
self._fn = fn
self._args = args
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self._fn(*self._args, *args, **kwargs)
With this proposal, the entire ladder collapses to a single generic signature:
from __future__ import annotations
from typing import Callable, Concatenate
class Wrapper[**P, R, *Ts]:
def __init__(self, fn: Callable[Concatenate[*Ts, P], R], *args: *Ts) -> None:
self._fn = fn
self._args = args
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R:
return self._fn(*self._args, *args, **kwargs)
def f(a: str, b: int, *, flag: bool = False, x: float) -> bool: ...
# Ts = () -> P = (a: str, b: int, *, flag: bool = ..., x: float)
w0 = Wrapper(f)
r0 = w0("hello", 42, x=3.14, flag=True) # type: bool
# Ts = (str,) -> P = (b: int, *, flag: bool = ..., x: float)
w1 = Wrapper(f, "hello")
r1 = w1(42, x=3.14) # type: bool
# Ts = (str, int) -> P = (*, flag: bool = ..., x: float)
w2 = Wrapper(f, "hello", 42)
r2 = w2(x=3.14) # type: bool
Specification
Grammar
Update the Concatenate grammar in the typing specification from:
concatenate ::= "Concatenate" "[" type_expression ("," type_expression)* "," parameter_specification_variable "]"
to:
concatenate_prefix_item ::= type_expression | unpack_expression
concatenate ::= "Concatenate" "[" concatenate_prefix_item ("," concatenate_prefix_item)* "," parameter_specification_variable "]"
Semantics
Expansion follows existing PEP 646 semantics: when *Ts is bound to tuple[T1, T2, ..., Tn], Concatenate[*Ts, P] is equivalent to Concatenate[T1, T2, ..., Tn, P]. When *Ts is bound to tuple[()], Concatenate[*Ts, P] simplifies to P. Individual type expressions and unpack expressions may be freely combined in the prefix (e.g. Concatenate[LeadingArg, *Ts, P]).
Open Question: Splitting Boundary
The core design question is: how does a type checker determine the split between the prefix and ParamSpec P?
When the prefix length is statically known, splitting is unambiguous. This covers concrete bounded tuples (Concatenate[*tuple[int, str], P] — always length 2) and value-anchored TypeVarTuples where a companion *args: *Ts pins the length at the call site (the Wrapper example above). These are the primary use cases.
Ambiguity arises when the prefix length is not statically determined:
Case A — Unanchored *Ts (no companion *args: *Ts):
class TaskRunner[**P, R, *Ts]:
def __init__(self, fn: Callable[Concatenate[*Ts, P], R]) -> None: ...
def compute(user_id: int, query: str, *, timeout: float = 5.0) -> bool: ...
# How many positional params should *Ts capture vs. leave in P?
task = TaskRunner(compute)
Case B — Unbounded tuple (*tuple[T, ...]):
def strip_leading_ints[**P, R](
fn: Callable[Concatenate[*tuple[int, ...], P], R]
) -> Callable[P, R]: ...
def example(x: int, y: int, z: int, *, flag: bool = False) -> None: ...
# *tuple[int, ...] could match 0, 1, 2, or 3 leading int parameters.
wrapped = strip_leading_ints(example)
Options:
-
Option 1 — Greedy prefix: The prefix consumes all matching positional-capable parameters. In Case A,
*Ts = (int, str)andP = (*, timeout: float = 5.0). In Case B, all 3 ints are consumed, leavingP = (*, flag: bool = False). -
Option 2 — Restrict to fixed-length prefixes initially: Require the prefix length to be statically determined (concrete bounded tuples, companion
*args: *Ts, or explicit specialization). Reject unanchored/unbounded prefixes as ambiguous and defer them to a future extension.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
まず、使用可能な位置について説明している、リンク先の typing 仕様のセクションを確認し、次に、ここで説明されている PEP 646 および PEP 612 の挙動と、提案されている文法および意味論を比較します。ParamSpec P のアンパックされたプレフィックスを分割する規則を選択して文書化し、アンカーされていないケースと無制限のケースも含めれば完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- python
- 領域
- devtools, documentation
- issue の種類
- 機能追加
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 活発さ
- 静か
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 35/100