keyword-variadic packs¶
in a .by file the PEP-695 spelling [**Kwargs] declares a keyword-variadic pack — an ordered
mapping of parameter name to type — rather than a ParamSpec:
class A[**Kwargs]:
def get(self) -> (**Kwargs) -> None:
raise NotImplementedError
def f(a: A[foo=int, bar=str]):
a.get()(foo=1, bar="x")
a pack is a true variadic: its fields are written inline at the specialization site, and unpacking
it with ** splices them into a parameter list. a ParamSpec by contrast is opaque — it can only
be forwarded whole, as the *args: *P / **kwargs: **P pair
specialization¶
a pack is specialized by keyword. every keyword type argument is one of its fields:
A[()] is the empty pack. field order is source order, and it is significant — A[foo=int, bar=str] and A[bar=str, foo=int] are different types
unpacking¶
**Kwargs in a callable arrow contributes the pack's fields as keyword-only
parameters:
class A[**Kwargs]:
def get(self) -> (**Kwargs) -> None: ...
def f(a: A[foo=int, bar=str]):
reveal_type(a.get()) # (*, foo: int, bar: str) -> None
a prefix works too — (int, **Kwargs) -> None is (int, /, *, foo: int, bar: str) -> None
inference from keyword arguments¶
**kwargs: **Kwargs unpacks the pack into a parameter list. the star count follows the pack's
declaration — [**Kwargs] unpacks with **, the way [*Ts] unpacks with * — so a single star
on a pack is a syntax error. a call's keyword arguments then solve the pack, one field each, in
source order:
class A[**Kwargs]:
init(**kwargs: **Kwargs)
a = A(x=1, y="s")
reveal_type(a) # final A[x=int, y=str]
the pack is solved as a whole rather than per-argument, so A() gives the empty pack A[()]. field
types are promoted the way any inferred type argument is, so x=1 yields int, not Literal[1]
alongside ordinary type variables¶
a pack claims every keyword argument, so the other type variables of the same class are given positionally:
class Two[T, **Kwargs]:
def get(self) -> (T, **Kwargs) -> None: ...
def f(t: Two[bytes, foo=int]):
reveal_type(t.get()) # (bytes, /, *, foo: int) -> None
this is why a pack's context can't also use the
keyword-subscript form for binding typevars by name: with a pack in scope,
A[foo=int] always names a field
scope¶
the keyword-pack reading is confined to .by. .byi is the interop surface with python's typing
ecosystem, where **P means ParamSpec, so a PEP-695 **P in a stub is still a ParamSpec.
a ParamSpec is a type variable bound by the top parameters form —
class A[P: (*: *, **: *)] — an anonymous variadic and an anonymous keyword-variadic, both
admitting anything. every parameter list is a subtype of it, so the bound ranges over all parameter
lists. that is what a python ParamSpec reverse-transpiles to, so class A[**P] round-trips, and
it is the form the vendored typeshed uses. the legacy P = ParamSpec("P") form also works, and the
arrow syntax unpacks any of them
lowering¶
class A[**Kwargs] is a ParamSpec at runtime, and python has no keyword subscript, so fields
lower to the ParamSpec list form:
transpiles to:
field names are erased, matching python's own erasure of type arguments — the names are checked
against the .by source, not the emitted python
**kwargs: **Kwargs takes the ParamSpec spelling, which is what the pack is at runtime:
transpiles to:
splicing into a dict literal type¶
{**Kwargs} contributes the pack's fields to a dict literal type. the
pack contributes nothing until it is specialized, and it is spliced in after the keys written
inline, so a field it carries wins over a key of the same name:
class A[**Kwargs]:
init(**args: **Kwargs)
def get(self) -> {"tag": int, **Kwargs}: ...
a = A(a=1, b="s")
reveal_type(a.get()) # {"a": int, "b": str, "tag": int}
before specialization the pack shows as a pending splice — {"tag": int, **Kwargs@A}
limitations¶
- a declared type does not drive a constructor call's arity.
A[foo=int]()correctly reports the missingfooargument, because the pack is already concrete when the parameters are matched;a: A[foo=int] = A()does not, because the pack is only pinned afterwards, by inference. worse, the declared pack is then adopted unchecked, soa: A[foo=int] = A(bar=1)is accepted - a type variable reachable only through a
{**Kwargs}splice reads as bivariant, so a class whose only use of its pack is{**Kwargs}does not reject a mismatched specialization. giving the pack a variance there is what surfaces the adoption bug above, so the two have to be fixed together. spelling the pack in an arrow as well restores invariance - a pack cannot yet be forwarded as a type argument (
A[**Kwargs]inside another signature) - a pack cannot have a default (
class D[**Kwargs = ()]) - a generic context containing a pack skips literal promotion for its other type variables, so
class Two[T, **Kwargs]infersTasLiteral[...]. this is pre-existing ty behaviour, shared withParamSpec
see also¶
- bounds on a variadic pack —
**Kwargs: intand**Kwargs: **{"a": int} - callable arrow syntax — the
**unpacking position - keyword arguments in subscripts — binding typevars by name
ParamSpec/Concatenatearrow callables