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

  1. Do not overwrite `__builtins__` with a non-dict; if you must inject globals, keep it a dict (e.g. `{'__builtins__': real_builtins_dict, ...}`)
  2. Restore the standard dict for `__builtins__` before code paths that use pyo3 frame introspection
  3. Avoid pyo3 `PyFrame::builtins()` on frames from sandboxed/exec-with-custom-builtins code; read the attribute dynamically and type-check instead
  4. 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

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


AI-assisted analysis of PyO3/pyo3@ac9b6899d3 (2026-09-05). Data as JSON: /api/errors/8bc8f70325af23a0. Report an issue: GitHub.