`eval('bar')` causes a `NameError`, but `print(bar); eval('bar')` doesn't

Question:

The following code

1 def foo():
2     def bar():
3         return 'blah'
4     def baz():
5         eval('bar')
6     baz()
7 foo()

causes a NameError — the entire error message is

Traceback (most recent call last):
  File "main.py", line 7, in <module>
    foo()
  File "main.py", line 6, in foo
    baz()
  File "main.py", line 5, in baz
    eval('bar')
  File "<string>", line 1, in <module>
NameError: name 'bar' is not defined

On the other hand, the following, almost identical code,

1 def foo():
2     def bar():
3         return 'blah'
4     def baz():
5         print(bar); eval('bar')
6     baz()
7 foo()

does not cause any error. In fact, it prints <function foo.<locals>.bar at 0x7fd5fdb24af0>.

So I was wondering why, in the first piece of code, is the variable named bar not defined/available in line 5. And why does printing bar before calling eval('bar') fix this issue. From my understanding bar should be a local variable of function foo, so it should be accessible from function foo.baz. Is it something about eval that screws this up that printing beforehand fixes?

Asked By: joseville

||

Answers:

>>> help(eval)
Help on built-in function eval in module builtins:

eval(source, globals=None, locals=None, /)
    Evaluate the given source in the context of globals and locals.
    
    The source may be a string representing a Python expression
    or a code object as returned by compile().
    The globals must be a dictionary and locals can be any mapping,
    defaulting to the current globals and locals.
    If only globals is given, locals defaults to it.

In your first example bar is not in local scope but in your second example it is in local because the statement print(bar) forces a scope resolution lookup before eval executes. (In neither example is bar in global scope.)

There are a couple of options you can use to pull bar into your desired scope:

# option 1: nonlocal (better)
def foo():
    def bar():
        return 'blah'
    def baz():
        nonlocal bar
        eval('bar');
    baz()
foo()

# option 2: pass a copy of actual scope to eval (less better)
def foo():
    def bar():
        return 'blah'
    def baz():
        eval('bar', None, local_scope);  # or eval('bar', local_scope)
    local_scope = locals()
    baz()
foo()
Answered By: Woodford