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.
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
- 0 < N <= 12
- Attaching a thread to the interpreter is currently…
- Attaching a thread to the interpreter is prohibited while a…
AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05).
Data as JSON: /api/errors/eca3bc907140a9a2.
Report an issue: GitHub.
Appendix: 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)