{"record":{"id":"f16b6b173adc9746","repo":"PyO3/pyo3","slug":"dictionary-keys-changed-during-iteration","errorCode":null,"errorMessage":"dictionary keys changed during iteration","messagePattern":"dictionary keys changed during iteration","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/types/dict.rs","lineNumber":557,"sourceCode":"                // or added during iteration, this will panic next time when `next` is called\n                if *di_used != ma_used {\n                    *di_used = -1;\n                    panic!(\"dictionary changed size during iteration\");\n                };\n\n                // If the dict is changed in such a way that the length remains constant\n                // then this will panic at the end of iteration - similar to this:\n                //\n                // d = {\"a\":1, \"b\":2, \"c\": 3}\n                //\n                // for k, v in d.items():\n                //     d[f\"{k}_\"] = 4\n                //     del d[k]\n                //     print(k)\n                //\n                if *remaining == -1 {\n                    *di_used = -1;\n                    panic!(\"dictionary keys changed during iteration\");\n                };\n\n                let mut key: *mut ffi::PyObject = core::ptr::null_mut();\n                let mut value: *mut ffi::PyObject = core::ptr::null_mut();\n\n                if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } {\n                    *remaining -= 1;\n                    let py = dict.py();\n                    // Safety:\n                    // - PyDict_Next returns borrowed values\n                    // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null\n                    Some((\n                        unsafe { key.assume_borrowed_unchecked(py).to_owned() },\n                        unsafe { value.assume_borrowed_unchecked(py).to_owned() },\n                    ))\n                } else {\n                    None\n                }","sourceCodeStart":539,"sourceCodeEnd":575,"githubUrl":"https://github.com/PyO3/pyo3/blob/ac9b6899d348be4d54614d060dea53a645a12e36/src/types/dict.rs#L539-L575","documentation":"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.","triggerScenarios":"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.","commonSituations":"Renaming keys in-place while iterating; callbacks that re-key dict entries; caches pruning expired entries while another component iterates them.","solutions":["Collect keys first and iterate the snapshot, applying renames afterwards","Use dict.copy() or items() to iterate over an immutable view","Restructure mutation into a post-iteration pass","Ensure no Python callbacks executed during iteration mutate the dict's key set"],"exampleFix":"// before\nfor (k, v) in dict.iter() { dict.set_item(new_key(k), v)?; dict.del_item(k)?; } // panics\n// after\nlet items: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect();\nfor (k, v) in items { dict.set_item(new_key(k), v)?; dict.del_item(k)?; }","handlingStrategy":"validation","validationCode":"// detect key churn risk: do not re-key dicts inside iteration\nlet items: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect();\nfor (k, v) in items { let _ = (k, v); /* safe to mutate now */ }","typeGuard":null,"tryCatchPattern":"std::panic::catch_unwind(|| { for kv in dict.iter() { /* read-only */ } }).unwrap_or_else(|_| { /* retry with copied dict */ });","preventionTips":["Snapshot (copy/items()) before renaming or re-keying dictionary entries","Keep iteration bodies free of inserts paired with deletes","Use dict.copy() when passing the dict into code that may mutate it"],"tags":["pyo3","dict","iteration","mutation","panic"],"backgroundTag":"dictionary-keys-changed-during-iteration","analyzedSha":"ac9b6899d348be4d54614d060dea53a645a12e36","analyzedAt":"2026-09-05T09:20:35.319Z","contentChangedAt":"2026-09-05T09:20:35.319Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}