← board

setattr is absent

p = P()
setattr(p, "y", 7)      # pascal26: error: undefined variable (setattr)
print(getattr(p, "y"))  # ...while THIS half has been here all along

Found 2026-08-15 by a CPython differential sweep over the builtin surface, one name at a time. Loud, and trivially worked around by p.y = 7 for a LITERAL name — but the shape setattr exists for is the computed one, setattr(o, "f_" + k, v) in a loop, which has no other spelling.

Fix

The trio's three members share one store: PyMakeDynAttrSet for a literal name (what o.name = v compiles to), and a new PyMakeDynAttrSetByExpr for a computed one — the mirror of PyMakeDynAttrByExpr, which getattr/hasattr have had for computed names since that path landed. A name resolving to a DECLARED field builds the ordinary field store, so setattr(p, "x", 5) and p.x = 5 write the same slot rather than two.

A user def setattr shadows it, which is Python's rule and the pattern this family of intercepts follows.

Also absent, measured in the same sweep, NOT fixed here

delattr, globals(), locals() — re-filed as [[bug-nilpy-delattr-globals-and-locals-are-absent]]. delattr needs a runtime entry that does not exist (there is no pydynattr_del to call), and globals/locals want a run-time name table this dialect deliberately does not build — the same answer exec(src) with no namespace already gives.

Gate

test/test_nilpy_setattr.npy (+.expected, in the Makefile), byte-identical to CPython: a declared field written both ways (one slot, not two); an undeclared attribute through the dynamic store, read back by all three of p.y, getattr and hasattr; a computed name from a variable and from a concatenation; the loop idiom; overwriting with a str, a list and None; the absent-attribute rows unchanged; and a second instance not seeing the first's dynamic attributes. gate.sh quick GREEN. No pin — frontend-only.