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

  1. Collect keys first and iterate the snapshot, applying renames afterwards
  2. Use dict.copy() or items() to iterate over an immutable view
  3. Restructure mutation into a post-iteration pass
  4. 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

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


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