{"record":{"id":"eca3bc907140a9a2","repo":"PyO3/pyo3","slug":"dictionary-changed-size-during-iteration","errorCode":null,"errorMessage":"dictionary changed size during iteration","messagePattern":"dictionary changed size during iteration","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/types/dict.rs","lineNumber":542,"sourceCode":"        &mut self,\n        dict: &Bound<'py, PyDict>,\n    ) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> {\n        match self {\n            Self::DictIter {\n                di_used,\n                remaining,\n                ppos,\n                ..\n            } => {\n                let ma_used = dict_len(dict);\n\n                // These checks are similar to what CPython does.\n                //\n                // If the dimension of the dict changes e.g. key-value pairs are removed\n                // 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();","sourceCodeStart":524,"sourceCodeEnd":560,"githubUrl":"https://github.com/PyO3/pyo3/blob/ac9b6899d348be4d54614d060dea53a645a12e36/src/types/dict.rs#L524-L560","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nfor (k, _) in dict.iter() { dict.del_item(k)?; } // panics\n// after\nlet keys: Vec<Bound<PyAny>> = dict.keys().iter().map(|k| k.unbind().into_bound(py)).collect();\nfor k in keys { dict.del_item(&k)?; }","handlingStrategy":"validation","validationCode":"// iterate a snapshot instead of the live dict\nlet snapshot: Vec<_> = dict.items().iter().map(|i| i.unbind()).collect();\n// mutate only after iteration","typeGuard":null,"tryCatchPattern":"std::panic::catch_unwind(|| { for kv in dict.iter() { /* no mutation here */ } }).unwrap_or_else(|_| { /* redo with snapshot */ });","preventionTips":["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"],"tags":["pyo3","dict","iteration","mutation","panic"],"backgroundTag":"dictionary-changed-size-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"}