'scope of globals, locals, use with exec()

I'm having a hard time understanding the scope of globals(), locals() in Python. Consider the following code:

def f1(glob, loc):
    exec("b = 5", glob, loc)


f1(globals(), locals())
print(b + 1)

This passes globals and locals to f1 where the variable b = 5 is defined, and sure enough it's present after the function call in the main scope. The code correctly prints

6

To my surprise, when doing the same in the scope of a function, f2 here, it fails:

def f1(glob, loc):
    exec("b = 5", glob, loc)


def f2():
    f1(globals(), locals())
    print(b + 1)


f2()
Traceback (most recent call last):
  File "/tmp/q.py", line 10, in <module>
    f2()
  File "/tmp/q.py", line 7, in f2
    print(b + 1)
NameError: name 'b' is not defined

Why is that? Is it possible to generalize f1 to allow it to be called from f2 as well?



Solution 1:[1]

In your first example, locals is the globals dict, because you're on a module level.

You can check that with print(id(globals()), id(locals()))

In your second example, locals is a different dict in the scope of the function.

So to make example 2 work, either use:

def f1(glob, loc):
    exec("b = 5", glob, loc)
def f2():
    f1(globals(), globals())
    print(b + 1)
f2()

or

def f1(glob, loc):
    exec("b = 5", glob, glob)
def f2(loc):
    f1(globals(), loc)
    print(b + 1)
f2(locals())

So this isn't really a matter of exec() but of in which scope you're calling locals().

And ofc it's also a matter of what the code in exec does, because an assignment changes the scope of the name to the current (local) scope, unless global/nonlocal is involved. That's why b=5 is executed in the local scope - except that in your first example, the "local" scope is the global scope.


Follow-up:

if you want to re-assign a value to b in f2 you will need the global keyword:

def f1(glob, loc):
    exec("b = 5", glob, loc)
def f2():
    f1(globals(), globals())
    global b
    b += 1
    print(b + 1) # 7
f2()

Note that this has nothing to do with exec and passing around locals() / globals() - you can reduce it to this example, which would also fail w/o the global keyword (it would throw an UnboundLocalError: local variable 'b' referenced before assignment error).

b = 5
def f2():
    global b
    b += 1
    print(b + 1) # 7
f2()

Solution 2:[2]

This isn't an explanation answer (jason's comment seems to be a good one though), but at work I've coded something that uses exec, and I've found exec(code, globals()) is the only way that worked under all circumstances. Adding locals as an argument only caused various issues.

To adapt your code:

def f1(glob):
    exec("b = 5", glob)

def f2():
    f1(globals())
    print(b + 1)

f2()
# 6

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Peter