Why can you change globals().__getitem__ but not __setitem__?

If you use a custom dict subclass for a function’s globals, you can override __getitem__ and this will be respected because the interpreter when processing LOAD_GLOBAL calls PyMapping_GetOptionalItem which respects user overrides. But STORE_GLOBAL results in a call to PyDict_SetItem, and that call assumes the exact dictionary type and ignores user overrides. It seems weird for it to not be symmetrical.

The LOAD_GLOBAL code (see below) has a fast path for when it’s really a regular dictionary. Any reason to not also do that in the STORE_GLOBAL case?

Test case demonstrating behavior:

import types

class MyGlobals(dict):
    def __getitem__(self, key):
        print(f"__getitem__ called for key: {key}")
        return super().__getitem__(key)

    def __setitem__(self, key, value):
        print(f"__setitem__ called for key: {key} with value: {value}")
        super().__setitem__(key, value)

def func():
    global x, y
    x = 10
    y = x + 5
    print(f"x = {x}, y = {y}")

custom_globals = MyGlobals()
custom_globals.update(func.__globals__)

func = types.FunctionType(
    func.__code__,
    custom_globals,
    name=func.__name__,
    argdefs=func.__defaults__,
    closure=func.__closure__
)

func()

print(f"Value of 'x' in custom globals: {custom_globals.get('x')}")
print(f"Value of 'y' in custom globals: {custom_globals.get('y')}")

Which gives this output (notice the __setitem__ print is missing):

__getitem__ called for key: x
__getitem__ called for key: print
__getitem__ called for key: x
__getitem__ called for key: y
x = 10, y = 15
Value of 'x' in custom globals: 10
Value of 'y' in custom globals: 15

Code for loading globals:

void
_PyEval_LoadGlobalStackRef(PyObject *globals, PyObject *builtins, PyObject *name, _PyStackRef *writeto)
{
    if (PyDict_CheckExact(globals) && PyDict_CheckExact(builtins)) {
        _PyDict_LoadGlobalStackRef((PyDictObject *)globals,
                                    (PyDictObject *)builtins,
                                    name, writeto);
        if (PyStackRef_IsNull(*writeto) && !PyErr_Occurred()) {
            /* _PyDict_LoadGlobal() returns NULL without raising
                * an exception if the key doesn't exist */
            _PyEval_FormatExcCheckArg(PyThreadState_GET(), PyExc_NameError,
                                        NAME_ERROR_MSG, name);
        }
    }
    else {
        /* Slow-path if globals or builtins is not a dict */
        /* namespace 1: globals */
        PyObject *res;
        if (PyMapping_GetOptionalItem(globals, name, &res) < 0) {
            *writeto = PyStackRef_NULL;
            return;
        }
        if (res == NULL) {
            /* namespace 2: builtins */
            if (PyMapping_GetOptionalItem(builtins, name, &res) < 0) {
                *writeto = PyStackRef_NULL;
                return;
            }
            if (res == NULL) {
                _PyEval_FormatExcCheckArg(
                            PyThreadState_GET(), PyExc_NameError,
                            NAME_ERROR_MSG, name);
            }
        }
        *writeto = PyStackRef_FromPyObjectSteal(res);
    }
}

Versus for storing:

        TARGET(STORE_GLOBAL) {
            frame->instr_ptr = next_instr;
            next_instr += 1;
            INSTRUCTION_STATS(STORE_GLOBAL);
            _PyStackRef v;
            v = stack_pointer[-1];
            PyObject *name = GETITEM(FRAME_CO_NAMES, oparg);
            _PyFrame_SetStackPointer(frame, stack_pointer);
            int err = PyDict_SetItem(GLOBALS(), name, PyStackRef_AsPyObjectBorrow(v));
            stack_pointer = _PyFrame_GetStackPointer(frame);
            PyStackRef_CLOSE(v);
            if (err) goto pop_1_error;
            stack_pointer += -1;
            assert(WITHIN_STACK_BOUNDS());
            DISPATCH();
        }
1 Like

Support for LOAD_GLOBAL calling __getitem__ looks like it was added in Python 3.3. The motivation was reduced memory usage and improved speed by sharing dictionary keys. I don’t seen any discussion of global variables or __getitem__ on non-dictionaries, so I suspect that change was incidental.

1 Like

There is previous discussion (and rejection) of __setitem__ on globals here:

1 Like