A keyword argument through a class VALUE is refused at run time
class P:
def __init__(self, a, b=2):
self.a = a
self.b = b
def kw(self):
return self.__class__(self.a, b=99)
print(P(5).kw().b)
CPython: 99
pxx: Unhandled exception: TypeError: a keyword argument through this kind of
callable value is not supported yet (an interpreted closure and a class
reached as a value still carry no parameter names)
P(5).kw() with the class named literally — P(self.a, b=99) — answers 99.
Same call, same defaults, different spelling of the callee.
PRE-EXISTING
Measured on stable_linux_amd64/default/pinned (v384), 2026-08-27, from the
sibling sweep of [[bug-n-self-class-cannot-be-called-as-a-constructor]]. That
ticket made self.__class__(...) reach the classref call path, which is why the
limitation is now easy to hit from ordinary code; the limitation itself is
older and belongs to [[feature-nilpy-class-as-a-value]].
The message is out of date, which is the lead
It says the callee "carries no parameter names". defs.inc says otherwise, at
the RTTI_METH_STARIDX_SHIFT block:
paramKinds points at 2*arity words:
aritykind words THENarityparam-NAME pointers. [...] Names let a reflected caller bind a KEYWORD argument by name instead of by position.
and PyClassRefNew is named in that same comment as the reflected caller. So
the names are present in the RTTI it already walks; binding by them is the work,
and the refusal predates them being there. Check that before designing
anything — if the names are populated for a NilPy-lowered __init__ the fix
is local to PyClassRefNew; if they are only populated for Pascal classes it is
a wider job in rtti_emit.inc.
Defaults are the second half: cls(x, b=99) must still apply __init__'s own
default for any parameter the caller skipped, which is what the positional path
gets for free today by construction.
Severity
A loud, accurate-shaped refusal rather than a wrong value — it names the
limitation and does not corrupt anything. That is why it is 55 and not higher.
The cost is that the standard self.__class__(...) idiom silently only supports
its positional half, and a caller discovers which half at run time.