← board

Mutating a dict during iteration is silently ignored

d = {"a": 1, "b": 2}
for k in d:
    d["c"] = 3
print(len(d))

CPython raises RuntimeError: dictionary changed size during iteration. NilPy runs the loop to completion over the ORIGINAL keys and prints the new length.

Why it happens, and why the list fix did not change it

for k in d does not iterate the dict. The frontend rewrites it to d.keylist() — a snapshot COPY — and then iterates that list (ci := listCi right after the keylist call in PyParseForIn). So the loop can never see a concurrent mutation, and making the loop bound live for lists left this untouched, because the live count now reads the copy.

The divergence is real but mild

It is silent, which normally makes it high priority here. It is prio 35 anyway because the program CPython would reject is one that is already relying on undefined-ish behaviour, and NilPy's answer — iterate the keys as they were at loop entry — is the defensible one that several languages choose. The cost of matching CPython is a modification counter on TPyDict checked each iteration.

This is a Track U question as much as a bug: do we raise (CPython parity) or keep the snapshot (defensible, and arguably nicer)? If the answer is "keep the snapshot", this ticket becomes a documentation item rather than a fix, and the behaviour should be stated explicitly rather than left as an accident of the keylist rewrite. Recommendation: keep the snapshot, document it, and close — raising costs a per-iteration check on every dict loop to reject programs that are already broken.

Gate

Whichever way it is decided, a .npy pinning the chosen behaviour, plus a note in the NilPy semantics doc alongside the other deliberate [[project_nilpy_semantics_vs_pascal_shared_layers]] splits.

2026-08-04 — escalated, not decided here

This ticket says "this is a Track U question as much as a bug" and then sits in a bug queue where nobody can answer it. Filed as [[decide-nilpy-dict-mutation-during-iteration]] with the three options (keep the snapshot and document it; match CPython with a modification counter; match it only under a strict flag) and this ticket's own recommendation carried across.

Also carried across, because it is the part most likely to read as a bug later: the LIST half is already settled the OTHER way — a list loop's bound is LIVE since 4eadf7f54, so for x in xs DOES observe mutation. Whichever way the dict goes, the asymmetry needs to be written down as chosen.

blocked-by set; no code touched.

2026-08-04 — DECIDED as intended behaviour; postponed

[[decide-nilpy-dict-mutation-during-iteration]] resolved: keep the snapshot, document it. So this is no longer a bug — it is devdocs/dev/nilpy-semantics-divergences.md, pinned by test/test_nilpy_dict_mutation_during_iteration.npy.

What remains is the OPTIONAL half the decision explicitly kept open: a --strict-python mode that raises RuntimeError on the modification, paying a per-iteration check to buy CPython parity for the differential sweeps. That is a feature nobody needs yet, so it moves to rainy-day/ rather than staying in a bug queue.

The measured correction that went with the decision, worth keeping here too: it is NOT "the same failure with a different message". Only a delete-then-read raises; an insert, and a delete whose key is never re-read, both complete silently — and the latter still visits the deleted key.