PyO3/pyo3 · error
dictionary changed size during iteration
Error message
dictionary changed size during iteration
What it means
PyDict's Rust iterator (next_unchecked) mirrors CPython's dict iterator safety rule: if the dictionary's size (ma_used) changed since iteration started, the iterator panics with 'dictionary changed size during iteration' rather than skipping or duplicating entries. This matches the RuntimeError CPython raises for dict iteration mutation.
Source
Thrown at src/types/dict.rs:542
&mut self,
dict: &Bound<'py, PyDict>,
) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {
match self {
Self::DictIter {
di_used,
remaining,
ppos,
..
} => {
let ma_used = dict_len(dict);
// These checks are similar to what CPython does.
//
// If the dimension of the dict changes e.g. key-value pairs are removed
// 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();View on GitHub (pinned to ac9b6899d3)
Solutions
- Snapshot the entries first: iterate over a Vec collected from dict.items() before mutating
- Defer mutations until after iteration completes
- Wrap mutations in a separate pass keyed by pre-collected keys
- Guard shared dicts with the GIL discipline / avoid cross-thread mutation during iteration
Example fix
// before
for (k, _) in dict.iter() { dict.del_item(k)?; } // panics
// after
let keys: Vec<Bound<PyAny>> = dict.keys().iter().map(|k| k.unbind().into_bound(py)).collect();
for k in keys { dict.del_item(&k)?; } Defensive patterns
Strategy: validation
Validate before calling
// iterate a snapshot instead of the live dict let snapshot: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect(); // mutate only after iteration
Try / catch
std::panic::catch_unwind(|| { for kv in dict.iter() { /* no mutation here */ } }).unwrap_or_else(|_| { /* redo with snapshot */ }); Prevention
- Never mutate a dict while iterating it — snapshot with items()/keys() first
- Avoid Python callbacks during iteration that may modify the dict
- Synchronize cross-thread dict access so no mutation overlaps iteration
When it happens
Trigger: Adding or removing key-value pairs from a PyDict while a Rust-side iterator (or PyDict_Next-based loop wrapped by PyO3) is still running over it.
Common situations: Mutating a dict inside a Python callback invoked during iteration; concurrent threads modifying a shared dict while one thread iterates; collecting keys then modifying inside the same loop.
Related errors
- dictionary keys changed 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/eca3bc907140a9a2.
Report an issue: GitHub.