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

  1. Snapshot the entries first: iterate over a Vec collected from dict.items() before mutating
  2. Defer mutations until after iteration completes
  3. Wrap mutations in a separate pass keyed by pre-collected keys
  4. 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

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


AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05). Data as JSON: /api/errors/eca3bc907140a9a2. Report an issue: GitHub.