← board

del x on a plain variable is a silent no-op

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.