Where is it documented that exec() makes changes only in the locals dict?

glob = {}
loc = {}
exec(‘x = 1’, glob, loc)
‘x’ not in glob
assert loc[‘x’] == 1

I understand that exec() modifies its locals argument.
But where is this in the documentation?

EDIT: The answer is that the ‘x = 1’ statement makes x a local variable, so obviously the update will happen in loc.

EDIT. See also post 7.

>>> glob = {}
>>> loc = {}
>>> exec("global x; x = 1", glob, loc)
>>> "x" in glob
True
>>> "x" in loc
False

It’s quite happy to modify either dictionary.

exec is documented here.

exec has the same effect as directly executing code (except where differences are noted in the documentation). Since assigment x = 1 without a global declaration assigns to the local variable x, exec("x = 1", glob, loc) does too, as if it were executed under a class declaration:

class Spam:
    x = 1
    print(locals())

One difference which is not explicitly documented is that the locals doesn’t get populated with entries for __module__ and __qualname__. But this is because exec is not an actual class statement, if merely executes the code as if it were in class scope.

The exec() call itself might modify the globals in order to store a reference to __builtins__ if it doesn’t already exist. That’s all that exec() itself actually changes. Everything else is up to the executed code. For an extreme example, note that exec() can execute any code object that takes no parameters, and Python 3.11 adds support for closures. For example:

def f():
    x = 'spam'
    def g():
        nonlocal x
        x = 'eggs'
    return g
>>> g = f()
>>> g.__closure__[0].cell_contents
'spam'
>>> exec(g.__code__, closure=g.__closure__)
>>> g.__closure__[0].cell_contents
'eggs'

If exec() is passed source code, it compiles in “exec” mode, which creates top-level code that uses non-optimized locals, globals, and builtins. This is compiled in the same way as a module (as opposed to compiling the body of a def or class statement). The only difference is that a module is always executed in a frame in which globals and locals are the same dict, whereas exec() allows executing code with different globals and locals. That’s uncommon, so exec() also supports using the same dict for globals and locals if passed only a globals dict, e.g. exec(source_code, globals). If a variable is assigned to in top-level source code that’s compiled in “exec” mode, it is implicitly a local variable, unless explicitly declared global.

However, note that “exec” mode compiles code for a module. It’s documented in the doctsring of the compile() function, which states “mode must be ‘exec’ to compile a module”. For example, you’re allowed to use star imports when compiling in “exec” mode, which are allowed in a module but forbidden in a class body. Also, a class body can reference and modify non-local variables from an enclosing function scope. There is no way to compile such code when compiling in “exec” mode because it’s top-level code, so there’s no outer compilation context. Also, code compiled for a class body sets up a closure for function definitions that reference __class__ or use argumentless super(). The top-level code in an “exec” compile has no such context of building a class.

Something else about exec() that I’m confused about.

>>> x = 'glob'
>>> def f():
...     x = 'closure'
...     class C:
...             print(x)
...             exec('print(x)')
...             print(eval('x'))
...     def g():
...             print(x)
...             exec('print(x)')
...             print(eval('x'))
...     g()
...
>>> f()
closure
glob
glob
closure
closure
closure

Why does exec (and eval) behave differently in a class and in a function?

I thought they did not have access to the environment closure, But in a function, they do.

Is there anything implementation-dependent involved?
Will PEP 558/667 make the behavior well defined, and will it behave the same in class and function?

Code compiled in “exec” or “eval” mode is top-level code that looks up variables that aren’t explicitly declared global by first checking locals, and then globals, and then builtins. Thus the value of x in your exec() and eval() examples comes from either the locals mapping or the globals mapping of the currently executing frame. Both exec() and eval() default to using the globals and locals of the calling scope.

For a function scope such as g(), which uses fast locals, the locals mapping of the frame is actually a snapshot that gets updated with the current values of ‘local’ variables when it’s accessed, such as by locals(), vars(), exec(), or eval(). I put “local” in scare quotes because, as implemented currently, this includes not only the fast locals and local cell variables, but also all of the non-local variables in the closure of the function and its inner lexical scopes (including functions, comprehensions, generators, and class definitions). In other words, it includes the actual local variables plus all of the variables that are mapped to cell objects in the function’s __closure__ tuple. The non-local variable x is present in this snapshot because g() calls print(x).

In contrast to a function, the code of a class body does not use fast locals, i.e. it’s compiled to directly use its frame’s f_locals mapping for local variables. Getting the locals mapping in this case is thus not handled as a snapshot, so it does not include non-local variables from the closure. In your example, in the body of class C, the execution of exec('print(x)') and eval('x') thus finds no local variable x, but it does find global x, which references the string "glob".

EDIT:
I asked about standardization of the behavior as well. I figured that out for myself. Since exec/eval use locals() as their locals dict, it’s a function of what locals() produces. PEP 558/667 will standardize this so that in a function scope, locals() will be a snapshot of local variables, not including captured nonlocals. This is different from same as current behavior with CPython, where x is included because it is referenced in the exec’ed body.

I made another example, which does not have a print(x). In this case, x is not part of locals() and the exec() finds x = ‘glob’.

>>> x = 'glob'
>>> def f():
...     x = 'closure'
...     class C:
...             print(locals())
...             print(x)
...             exec('print(x)')
...             print(eval('x'))
...     def g():
...             print(locals())
...             print(x)
...             exec('print(x)')
...             print(eval('x'))
...     g()
...     def h():
...             print(locals())
...             exec('print(x)')
...             print(eval('x'))
...     h()
...
>>> f()
{'__module__': '__main__', '__qualname__': 'f.<locals>.C'}
closure
glob
glob
{'x': 'closure'}
closure
closure
closure
{}
glob
glob

No, with respect to the inclusion of closure variables in the mapping that locals() returns, PEP 558 standardizes the current behavior of CPython.

Thanks. I read 558 more carefully. Edit made to post 7.