del x on a plain variable is a silent no-op
- Type: bug (silent semantic divergence) — Track N
- Found: 2026-08-06, bughunting with
tools/pydiff.py. - Low priority:
delon a bare name is uncommon, and the container forms — which are the ones real code uses — are correct.
Measured (self-hosted at 54fbd2754)
x = 5
del x
print(x) # CPython NameError pxx 5
s = "hi"
del s
print(s) # CPython NameError pxx hi
lst = [1, 2]
del lst
print(lst) # CPython NameError pxx [1, 2]
def f():
y = 7
del y
return y # CPython UnboundLocalError pxx 7
print(f())
Module scope and def scope behave the same. No diagnostic in any case — the statement parses, compiles, and has no effect.
The container forms are correct and must stay so:
lst = [1, 2, 3]; del lst[1]; print(lst) # [1, 3] agrees
d = {"a": 1, "b": 2}; del d["a"]; print(d) # {'b': 2} agrees
Why it is worth fixing even at low priority
Silence is the problem, not the missing unbind. del on a name is written for
one of two reasons: to drop a reference so an object can be collected, or to
make a later accidental use fail loudly. NilPy grants neither, and the second
one inverts: code that used del as a guard rail gets the OPPOSITE of what it
asked for, with no sign.
It survives the upward-compatibility rule — checked
If code works on CPython, it must work on NilPy. Accepting what CPython rejects is a feature, not a defect. (User, 2026-08-06 — see
devdocs/dev/nilpy-semantics-divergences.md.)
Most "we are laxer than CPython" findings are NOT bugs under that rule, so this one was re-checked against it rather than assumed. It survives, because a program CPython accepts and runs to completion can observe the difference:
x = 5
del x
try:
print("read:", x)
except NameError:
print("gone")
CPython prints gone; pxx prints read: 5. Nothing is rejected on either side —
this is a working program giving two answers, which is the definition of the
bug.
How to land it
Actually unbind. It wants a notion of "bound" the frontend does not have
today (a NilPy local is a frame slot, always present), so it likely means a
sentinel plus a check on read — a real cost on every read of any name that is
ever del'd, which is why this sits at prio 30 rather than higher.
An earlier draft of this ticket recommended refusing del <name> outright, on
the general principle that a clear refusal beats a plausible wrong answer. That
is wrong here and is struck: del x is valid CPython, and refusing it would
break upward compatibility — the one direction that is not negotiable. Refusal
is the right answer for a form CPython also rejects, not for one it accepts.
Gate
Per-fix loop. A .npy test with del on a module-scope name, a def-local, and
both container forms (which must stay correct), diffed against CPython — or, if
option 2 is taken, a {%FAIL}-style expectation that the bare-name form is
rejected.