python / python/typing

Proposal: Support Unpacked `TypeVarTuple` and `tuple` in `Concatenate`

Ouverte
#2,332 0 commentaires 1 réaction 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

topic: feature
Langage dominant
Python
Étoiles
1.8k
Forks
302
Merge moyen
23 h
PR mergées (30 j)
8

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) and P = (*, timeout: float = 5.0). In Case B, all 3 ints are consumed, leaving P = (*, 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.

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

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.

Évaluation

Stack technique
python
Domaine
devtools, documentation
Type d'issue
Fonctionnalité
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
Calme
Clarté
Plutôt claire
Accessibilité débutants
35/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.