Proposal: Support Unpacked `TypeVarTuple` and `tuple` in `Concatenate`
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 5/5
- Temps estimé
- Plus d'une semaine
- Accessibilité débutants
- 35/100
- Type d'issue
- Fonctionnalité
- Clarté
- Plutôt claire
- Activité
- Calme
- Stack technique
- python
- Domaine
- devtools, documentation
Piste de recherche
Commencez par la section liée de la spécification de typing consacrée aux emplacements d’utilisation valides, puis comparez la grammaire et la sémantique proposées avec le comportement de PEP 646 et PEP 612 décrit ici. Le travail est terminé lorsqu’une règle de séparation du préfixe décompacté de ParamSpec P a été sélectionnée et documentée, y compris les cas non ancré et non borné.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
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.
- Langage dominant
- Python
- Étoiles
- 1.8k
- Forks
- 302
- Merge moyen
- 23 h
- PR mergées (30 j)
- 8
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de python/typing
-
topic: typing spec
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
-
topic: typing spec
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
-
topic: documentation
Difficulté 2/5 1-3 heures Accessibilité débutants 76/100
-
topic: documentation
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
-
topic: conformance tests topic: typing spec
Difficulté 3/5 1-2 jours Accessibilité débutants 72/100
Toutes les issues de python/typing
Issues similaires
-
[Bug] reef-hermes tells me to resume with hermes --resume, which does not work from my shell Ouvertearea: harness bug status: needs-triage
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
Human-Agent-Society/reef#625 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 70/100
-
Difficulté 1/5 Moins d'une heure Accessibilité débutants 80/100
learningequality/kolibri#15351 · 2 commentaires ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
-
Name consistency Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
eellak/triplestore#65 · 1 commentaire ·