← board

PyNoSetitemError does not name the class

class ReadOnly:
    def __getitem__(self, k): return k
ReadOnly()["x"] = 2
message
CPython 'ReadOnly' object does not support item assignment
pxx object does not support item assignment (no __setitem__)

Correct BEHAVIOUR — a TypeError, at run time, catchable — and the wrong wording. Small, but worth fixing because its own sibling already does it right: the READ refusal raises 'SetOnly' object is not subscriptable, naming the class exactly as CPython does (PyNotSubscriptableNode passes the name in). So the two halves of one feature disagree, and the half that reads worse is the one a user hits while debugging a write.

Also note the parenthetical (no __setitem__) is implementation-facing — it names the dunder rather than the class, which is backwards for a user-facing diagnostic.

Fix

PyNoSetitemError (pylib) takes no arguments. Give it the class name the way PyNotSubscriptableNode does, and pass it from the three call sites in parser.inc's subscript arm.

Gate

CORRECTED 2026-08-09: this needs no re-pin. compiler/compiler.pas does not link pylib (it uses SysUtils, Math, BaseUnix, asmcore), so a pylib change cannot move the compiler binary and the self-host fixedpoint still converges FROM PINNED — measured on [[bug-nilpy-a-variant-argument-binds-a-class-overload-and-is-unwrapped-unchecked]]. The A != B effect is specific to the builtin units the COMPILER itself links (builtinheap and friends), not to compiler/builtin/** as a directory. So this is ordinary per-fix-loop work. test_nilpy_not_subscriptable.npy already exercises both write cells; it asserts only the LABEL there, and would assert the full message once this lands.

Found by

[[bug-nilpy-setitem-without-getitem-write-does-not-compile]], whose 2x2 test made the asymmetry visible — the read cells could be diffed against CPython verbatim and the write cells could not.

FIXED 2026-08-09 — and the delete half had the same flaw

PyNoSetitemError and PyNoDelitemError now take the class name and produce CPython's exact wording:

'ReadOnly' object does not support item assignment
'Plain' object does not support item assignment
'NoDel' object does not support item deletion

Diffed against CPython, all three identical.

PyNoDelitemError was not in this ticket's scope and had the same defect — found by grepping for the sibling rather than waiting for it to be reported. Both also dropped the implementation-facing parenthetical ((no __setitem__)), which named the DUNDER where a user-facing diagnostic should name the CLASS.

One builder, not four call sites

PyClassErrCallNode(procName, ci) builds the call with the name argument, and the four sites (two __setitem__ refusals in parser.inc, the __delitem__ one in pyparser.inc, and the neither-member write arm) go through it. That is the point rather than tidiness: this ticket exists because the read refusal passed a class name and the write refusal did not, so one feature's halves drifted on wording. A single builder is what keeps them agreed.

ci < 0 (a variant receiver, no class in hand) yields the un-named message rather than inventing a name.

No re-pin

Per the correction on [[bug-nilpy-a-variant-argument-binds-a-class-overload-and-is-unwrapped-unchecked]]: compiler.pas does not link pylib, so the self-host fixedpoint still converges from pinned. This ticket's own "worth batching with the other queued pylib work rather than re-pinning for a message" reasoning was based on the over-broad rule and has been corrected above.

Gate

test_nilpy_not_subscriptable.npy now asserts the FULL messages where it previously asserted only a label — the label was a deliberate placeholder put there by the ticket that found this, and leaving it would have kept pinning the weaker claim. test_nilpy_delitem_dunder.npy still green. Self-host fixedpoint byte-identical; tools/gate.sh quick GREEN.

Log