PyO3/pyo3 · error
dictionary keys changed during iteration
Error message
dictionary keys changed during iteration
What it means
next_unchecked also detects the subtler case where the dict's length stays constant but keys are replaced (insert + delete during iteration), tracked via the 'remaining' sentinel. PyO3 panics with 'dictionary keys changed during iteration', the same condition CPython reports as RuntimeError since 3.x for dict key churn mid-iteration.
Source
Thrown at src/types/dict.rs:557
// or added during iteration, this will panic next time when `next` is called
if *di_used != ma_used {
*di_used = -1;
panic!("dictionary changed size during iteration");
};
// If the dict is changed in such a way that the length remains constant
// then this will panic at the end of iteration - similar to this:
//
// d = {"a":1, "b":2, "c": 3}
//
// for k, v in d.items():
// d[f"{k}_"] = 4
// del d[k]
// print(k)
//
if *remaining == -1 {
*di_used = -1;
panic!("dictionary keys changed during iteration");
};
let mut key: *mut ffi::PyObject = core::ptr::null_mut();
let mut value: *mut ffi::PyObject = core::ptr::null_mut();
if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } {
*remaining -= 1;
let py = dict.py();
// Safety:
// - PyDict_Next returns borrowed values
// - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null
Some((
unsafe { key.assume_borrowed_unchecked(py).to_owned() },
unsafe { value.assume_borrowed_unchecked(py).to_owned() },
))
} else {
None
}View on GitHub (pinned to ac9b6899d3)
Solutions
- Collect keys first and iterate the snapshot, applying renames afterwards
- Use dict.copy() or items() to iterate over an immutable view
- Restructure mutation into a post-iteration pass
- Ensure no Python callbacks executed during iteration mutate the dict's key set
Example fix
// before
for (k, v) in dict.iter() { dict.set_item(new_key(k), v)?; dict.del_item(k)?; } // panics
// after
let items: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect();
for (k, v) in items { dict.set_item(new_key(k), v)?; dict.del_item(k)?; } Defensive patterns
Strategy: validation
Validate before calling
// detect key churn risk: do not re-key dicts inside iteration
let items: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect();
for (k, v) in items { let _ = (k, v); /* safe to mutate now */ } Try / catch
std::panic::catch_unwind(|| { for kv in dict.iter() { /* read-only */ } }).unwrap_or_else(|_| { /* retry with copied dict */ }); Prevention
- Snapshot (copy/items()) before renaming or re-keying dictionary entries
- Keep iteration bodies free of inserts paired with deletes
- Use dict.copy() when passing the dict into code that may mutate it
When it happens
Trigger: During iteration over a PyDict: deleting a key and inserting a different key (size unchanged), e.g. d[k2]=v; del d[k1] inside the loop; also triggered when the used-count sentinel is set by prior mutation.
Common situations: Renaming keys in-place while iterating; callbacks that re-key dict entries; caches pruning expired entries while another component iterates them.
Related errors
- dictionary changed size during iteration
- Unknown return value from PyDict_SetDefaultRef: {x}
- Attaching a thread to the interpreter is prohibited while a
- Cannot attach to the Python interpreter while it is finalizi
- Attaching a thread to the interpreter is currently prohibite
AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05).
Data as JSON: /api/errors/f16b6b173adc9746.
Report an issue: GitHub.