@dataclass: a str field's DEFAULT is silently dropped (becomes '')
from dataclasses import dataclass
@dataclass
class D:
a: str
s: str = "t"
i: int = 7
f: float = 1.5
b: bool = True
d = D("x")
print(repr(d.s), d.i, d.f, d.b)
pxx: '' 7 1.5 True
CPython: 't' 7 1.5 True
The int, float and bool defaults sitting right beside it are all correct, which is exactly what makes this hide: the class looks like defaults work.
Silent — no diagnostic, and '' is a perfectly ordinary value, so it surfaces
far away as an empty name/path/label.
Confirmed pre-existing
Built from stable_linux_amd64/default/pinned as well as HEAD: both print
''. Not a regression from the repr work landed alongside this ticket
(the control removes the variable — a pinned BINARY, not a source revert).
Where it is NOT — four candidates eliminated by measurement
This took four rebuilds to narrow, so the dead ends are recorded rather than
left for the next session to re-walk. PXXDBG=n.ctorargs prints every
construction's argument kinds and types and is the probe that answers this.
The call site builds the default as [1]kind=2,tk=4 — an AN_STR_LIT tagged
tyString (frozen inline) — while the parameter it feeds is tyAnsiString
(managed). That mismatch is the fault. The question was who builds that node.
| candidate | eliminated by |
|---|---|
PyDcDefaultNode PYDC_STR (pyparser.inc) |
retagged it tyAnsiString → probe still printed tk=4; then made it emit an AN_INT_LIT marker → probe STILL printed kind=2. Not in the path at all. |
PyClsAttrNode PYDC_STR (pyparser.inc) |
retagged tyAnsiString → symptom unchanged; and its two call sites build ASSIGN statements, not args |
| the generated ctor body | PXXDBG=a.ir:D.create shows params a and s both tk=23 storing to tk=23 fields. The ctor is correct — the caller passes an empty string |
| the token capture | the class-attribute path (NAME = "hello") uses byte-identical capture code and reads back correctly |
The boundary — it is NOT "annotated str param with a default"
Varying the shape, every one of these is correct today:
def g(a: int, s: str = "t") -> str: ... # correct
def f(a, s="t"): ... # correct
class K:
def __init__(self, a: int, s: str = "t"): ... # correct
So DefaultArgValueNode (parser.inc:2649) builds a tyString literal for an
annotated str parameter in the ordinary cases and the conversion to a managed
AnsiString parameter works there. Only the generated dataclass ctor's call
site loses it. That is the remaining difference and where the next session
should start: what PyClassCreate does to a default argument that a plain
ParseCall does not.
Why it was not fixed in the same session
The remaining suspects are parser.inc and the argument lowering in ir*.inc
— shared core files under the sole-A guard, and this session could not confirm
sole-A. Same block as the four tickets behind
decide-sole-a-guard-for-unattended-sessions.
Gate when it lands
Extend test/test_nilpy_dataclass_repr.npy, which deliberately uses no
string defaults today and says why — pinning the wrong answer there would have
frozen this bug. Add a str default to its Mixed class and regenerate
.expected from CPython.
2026-08-09 — FIXED, and the earlier recon's four dead ends were all correct
PyClassCreate had a loop that filled every still-unsupplied STRING parameter
with a HARDCODED EMPTY LITERAL (SOffset := 0; SLen := 0), and it ran before
the dataclass-default loop. So it consumed the slot, and the default was never
consulted. Moving it to run LAST — after the dataclass defaults, filling only
what remains — is the whole fix.
That is why the earlier session could prove three separate default-builders
were NOT in the path and still not find it. PyDcDefaultNode, PyClsAttrNode
and DefaultArgValueNode were each markered with a distinctive AN_INT_LIT and
each failed to fire — all three answers were correct, because by then the
argument already existed. The eliminations were sound; the missing question was
"who filled this slot BEFORE any of them ran?"
The generalisable version, worth more than the fix: when every candidate
producer is proved absent, stop looking for a producer and ask what CONSUMED the
slot. The n.ctorargs probe showed an argument present with a plausible shape,
which reads as "someone built it wrongly" and not as "someone built it first".
Also confirmed by the same probe: the explicit and defaulted arguments are
both AN_STR_LIT with tk=4, so the node's type tag was never the difference
— an earlier hypothesis that cost a build to eliminate.
Verified against CPython: defaults given and omitted, several string defaults in
one class, a string default before and after other kinds, an EMPTY string
default (which must stay empty), and a hand-written __init__ with a string
default as the control. test/test_nilpy_dataclass_repr.npy was also extended
with the string default its own header promised to add once this was fixed —
placed LAST in the class, since trailing string slots are what the bug consumed.
gate.sh quick GREEN.