PyO3/pyo3 · warning
`PyFrame_GetBuiltins` returns a `dict`
Error message
`PyFrame_GetBuiltins` returns a `dict`
What it means
`PyFrame::builtins()` calls the C-API `PyFrame_GetBuiltins` and expects the result to be a `dict` (the documented type). Python code can legally overwrite a frame/module's `__builtins__` with an arbitrary object, so pyo3 panics for correctness instead of mis-typing the result. This affects Python 3.11+ with the non-limited C API.
Source
Thrown at src/types/frame.rs:162
#[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
fn builtins(&self) -> Bound<'py, PyDict> {
// SAFETY:
// - we're attached to the interpreter
// - `self` is a `PyFrameObject`
// - `PyFrame_GetBuiltins` returns an owned reference
// - the result can not be null
unsafe {
ffi::PyFrame_GetBuiltins(self.as_ptr().cast())
.assume_owned_unchecked(self.py())
.cast_into()
// The result is expected (and documented) to be a dict object, however it is
// possible for Python code to overwrite `__builtins__` with any arbitrary object.
// As reasonable code should never do this, we panic here for correctness in case
// the type does not match.
//
// See https://github.com/PyO3/pyo3/issues/6048
.expect("`PyFrame_GetBuiltins` returns a `dict`")
}
}
#[cfg(all(Py_3_11, not(Py_LIMITED_API)))]
fn globals(&self) -> Bound<'py, PyDict> {
// SAFETY:
// - we're attached to the interpreter
// - `self` is a `PyFrameObject`
// - `PyFrame_GetGlobals` returns an owned reference
// - the result can not be null
// - the result is a dict object
unsafe {
ffi::PyFrame_GetGlobals(self.as_ptr().cast())
.assume_owned_unchecked(self.py())
.cast_into_unchecked()
}
}
View on GitHub (pinned to ac9b6899d3)
Solutions
- Do not overwrite `__builtins__` with a non-dict; if you must inject globals, keep it a dict (e.g. `{'__builtins__': real_builtins_dict, ...}`)
- Restore the standard dict for `__builtins__` before code paths that use pyo3 frame introspection
- Avoid pyo3 `PyFrame::builtins()` on frames from sandboxed/exec-with-custom-builtins code; read the attribute dynamically and type-check instead
- Track/upgrade pyo3 (issue pyo3#6048) in case this becomes a fallible API
Example fix
# before
exec(code, {'__builtins__': MyFakeBuiltins()})
# after
import builtins
globals_dict = {'__builtins__': builtins.__dict__}
exec(code, globals_dict) Defensive patterns
Strategy: validation
Validate before calling
# Python: ensure __builtins__ is a real dict before frame introspection
import builtins
assert isinstance(globals_dict.get('__builtins__'), dict), 'keep __builtins__ a dict' Type guard
def has_dict_builtins(frame) -> bool:
return isinstance(frame.f_builtins, dict) Prevention
- Never replace __builtins__ with a non-dict object
- If sandboxing, pass {'__builtins__': builtins.__dict__} to exec
- Avoid pyo3 frame.builtins() on sandboxed frames
- Track pyo3#6048 for a fallible API
When it happens
Trigger: Inspecting frames (via `sys._getframe`, profilers, tracers, `inspect` module bridges) where `__builtins__` was replaced with a non-dict object; executing code compiled with `exec(code, {'__builtins__': something_not_a_dict})` and then reading `frame.builtins()` through pyo3.
Common situations: Sandboxes or instrumentation frameworks that substitute `__builtins__` (e.g. RestrictedPython-style setups, test harnesses, coverage/tracing tools) combined with Rust frame introspection.
Related errors
- Neither abi3 or abi3t features are enabled
- Cannot target an abi3t version below {MINIMUM_SUPPORTED_VERS
- failed to run the Python interpreter at {}: {}
- Python script failed
- cannot set a minimum Python version {} higher than the inter
AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05).
Data as JSON: /api/errors/8bc8f70325af23a0.
Report an issue: GitHub.