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.

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.

Appendix: 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)