Hi Carl! I used some of my Fable credit to have it write a prototype for mypy. It complied:
I have to admit that I didn’t review the code – it’s only a prototype --, but I ran it and it appears to work. It includes a fair amount of tests (including many edge cases) and they pass. You have to use it as mypy --enable-incomplete-feature SubscriptableFunctions to enable the new feature.
If we want such functions to be generic/subscriptable, the PEP should say so, and this would end up in the spec and mypy (and possibly others) would have to be changed. Alternatively, the PEP should explicitly say that unused typevars are stripped, and we should update the spec.
Note that this has consequences if only some of the typevars are used too – after def f[S, T](arg; T) -> T: ... the revealed signature is def [T] (arg: T) -> T and this affects subscripting: f[int] is valid but f[str, int] is not.
Going over Carl’s other questions, several of them have (partial) answers from the prototype:
Here are the worked examples, each paired with its class equivalent.
TypeVarTuple
python
def variadic[*Ts](*args: *Ts) -> tuple[*Ts]: ...
variadic[int, str] # def (int, str) -> tuple[int, str]
variadic[int, str](1, "a") # tuple[int, str]
variadic[int, str](1, 2) # error: arg 2 expected str
variadic[int] # def (int) -> tuple[int]
variadic[()] # def () -> tuple[()] — empty case works
Prefix and suffix TypeVars both bind correctly, matching the class forms exactly:
python
def pre[T, *Ts](x: T, *rest: *Ts) -> tuple[T, *Ts]: ...
pre[int, str, bytes] # def (x: int, str, bytes) -> tuple[int, str, bytes]
def suf[*Ts, T](*args: *Ts, last: T) -> tuple[*Ts, T]: ...
suf[int, str, bytes] # def (int, str, *, last: bytes) -> tuple[int, str, bytes]
That suffix case is Carl’s “TypeVarTuple followed by a regular TypeVar” question — it works, and greedy-middle absorption puts int, str in Ts and bytes in T, same as class SC[*Ts, T] yields SC[int, str, bytes].
PEP 646 unpacked-tuple disambiguation — his other explicit question — also works, in all three spellings:
python
variadic[*tuple[int, str]] # def (int, str) -> tuple[int, str]
variadic[Unpack[tuple[int, str]]] # same
variadic[*tuple[int, ...]] # def (*args: int) -> tuple[int, ...]
The homogeneous case behaves as for classes (VC[*tuple[int, ...]]), so the answer to “is it ever allowed?” is yes, with the same semantics.
ParamSpec
python
def wrap[**P, R](f: Callable[P, R]) -> Callable[P, R]: ...
wrap[[int, str], bool] # def (f: def (int, str) -> bool) -> def (int, str) -> bool
wrap[..., bool] # def (f: def (*Any, **Any) -> bool) -> ...
wrap[[], bool] # def (f: def () -> bool) -> ...
Concatenate and ParamSpec passthrough, inside a scope where the ParamSpec is bound:
python
def scope(g: Callable[P2, None]) -> None:
wrap[Concatenate[int, P2], bool] # def (f: def (int, *P2.args, **P2.kwargs) -> bool) -> ...
wrap[P2, bool] # def (f: def (*P2.args, **P2.kwargs) -> bool) -> ...
And both TypeVarTuple and ParamSpec together, Carl’s third question:
python
def both[*Ts, **P](*args: *Ts, cb: Callable[P, None]) -> None: ...
both[int, str, [bool]] # def (int, str, *, cb: def (bool))
On whether the class rules apply
Confirmed for every form I tested, but let me be precise about what that confirms. I ran each function case against a structurally identical generic class and compared: bracketed lists, ..., empty list, the bare-single-argument shorthand (one[int] → [int], matching OC[int] → OC[[int]]), Concatenate, passthrough, arity errors, and the mixed TVT+ParamSpec case. Every pair agreed on the resulting parameter list. Where they errored, they errored identically — including an unbound-ParamSpec-at-module-scope case where both produced the same two diagnostics.
The caveat: this confirms mypy’s function path and mypy’s class path agree. It doesn’t independently verify that mypy’s class behavior is itself spec-conformant, and it’s one checker. The mechanism behind the agreement is that my semanal change routes functions through the same allow_unpack / has_param_spec machinery classes use, so agreement is somewhat by construction — which is the point, but worth stating plainly rather than presenting as independent corroboration.
So the defensible claim for the PEP is: making ParamSpec and TypeVarTuple subscription follow the existing class rules is implementable by reusing existing machinery, and produces consistent results across every form in the spec. That’s enough to justify the PEP simply saying “same rules as generic classes” rather than restating them.