{"record":{"id":"f0ba9eb4c5994132","repo":"pydantic/monty","slug":"dict-view-must-always-reference-a-dict","errorCode":null,"errorMessage":"dict view must always reference a dict","messagePattern":"dict view must always reference a dict","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/monty/src/types/dict_view.rs","lineNumber":33,"sourceCode":"        iter::checked_preallocation_hint,\n    },\n    value::{EitherStr, Value},\n};\n\n/// Shared accessors for heap-backed dictionary view objects.\n///\n/// All dictionary views are thin live references to an underlying `dict`. They do\n/// not snapshot keys, items, or values; instead every observable operation reads\n/// through to the current dict state. Keeping that behavior centralized avoids\n/// subtle divergence between keys/items/values views.\npub(crate) trait DictView {\n    /// Returns the heap id of the underlying dictionary this view keeps alive.\n    fn dict_id(&self) -> HeapId;\n\n    /// Returns the live dictionary backing this view.\n    fn dict<'a>(&self, heap: &'a Heap) -> &'a Dict {\n        let HeapData::Dict(dict) = heap.get(self.dict_id()) else {\n            panic!(\"dict view must always reference a dict\");\n        };\n        dict\n    }\n}\n\n/// Live view returned by `dict.keys()`.\n///\n/// `dict_keys` is set-like in CPython, so this view supports the shared live-view\n/// behavior plus equality against other keys views and ordinary set-like values.\n/// The remaining set algebra operations are added incrementally in the VM layer.\n#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]\npub(crate) struct DictKeysView {\n    dict_id: HeapId,\n}\n\nimpl DictKeysView {\n    /// Creates a new keys view over an existing dictionary heap entry.\n    #[must_use]","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/pydantic/monty/blob/adc986b362e3961f407868cb118a99fe831b9e61/crates/monty/src/types/dict_view.rs#L15-L51","documentation":"A dict view (keys/items/values) stores the HeapId of the dictionary it was created from and must always find a Dict at that id; any other HeapData variant is impossible by construction, so the helper panics. Hitting it means heap corruption or a HeapId that outlived its entry and was reused for another object.","triggerScenarios":"Calling `dict(heap)` on a DictView trait object when the backing HeapId now points at a non-Dict entry — only reachable if refcount cleanup freed the dict and the id was recycled while the view stayed alive.","commonSituations":"Memory-model-checks runs after changes to heap cleanup or view lifetime tracking; contributors investigating heap corruption reports.","solutions":["Fix the lifetime/refcount path that allowed the view's HeapId to be freed or reassigned while the view lived.","Ensure views hold a counted reference to the dict (dec_ref participation via py_dec_ref_ids).","Audit any code that copies a view's dict_id without incrementing the refcount."],"exampleFix":"// before\nlet dict_id = view.dict_id(); // borrowed without refcount\n// after\nlet dict_id = view.dict_id();\nheap.inc_ref(dict_id); // keep the entry alive while borrowed","handlingStrategy":"type-guard","validationCode":"// verify the entry is still a Dict before using the view\nif !matches!(heap.get(view.dict_id()), HeapData::Dict(_)) { return Err(ViewError::stale_dict()); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Views must hold counted references to their backing dict","Never copy dict_id out of a view without inc_ref","Run memory-model-checks on changes to view or heap lifetime code"],"tags":["rust","heap","internal-invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"adc986b362e3961f407868cb118a99fe831b9e61","analyzedAt":"2026-09-13T19:19:18.698Z","contentChangedAt":"2026-09-13T19:19:18.698Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}