Eval scope in Python 2 vs. 3

Question:

I came across bizarre eval behavior in Python 3 – local variables aren’t picked up when eval is called in a list comprehension.

def apply_op():
    x, y, z = [0.5, 0.25, 0.75]
    op = "x,y,z"
    return [eval(o) for o in op.split(",")]
print(apply_op())

It errors in Python 3:

▶ python --version
Python 3.4.3
▶ python eval.py
Traceback (most recent call last):
  File "eval.py", line 7, in <module>
    print(apply_op())
  File "eval.py", line 5, in apply_op
    return [eval(o) % 1 for o in op.split(",")]
  File "eval.py", line 5, in <listcomp>
    return [eval(o) % 1 for o in op.split(",")]
  File "<string>", line 1, in <module>
NameError: name 'x' is not defined

And it works fine in Python 2:

▶ python --version
Python 2.7.8
▶ python eval.py
[0.5, 0.25, 0.75]

Moving it outside of the list comprehension removes the problem.

def apply_op():
    x, y, z = [0.5, 0.25, 0.75]
    return [eval("x"), eval("y"), eval("z")]

Is this intended behavior, or is it a bug?

Asked By: PattimusPrime

||

Answers:

There is a closed issue in the bug tracker for this: Issue 5242.

The resolution for this bug is won’t fix.

Some comments from the Issue read:

This is expected, and won’t easily fix. The reason is that list
comprehensions in 3.x use a function namespace “under the hood” (in 2.x,
they were implemented like a simple for loop). Because inner functions
need to know what names to get from what enclosing namespace, the names
referenced in eval() can’t come from enclosing functions. They must
either be locals or globals.

eval() is probably already an hack, there’s no need to add another hack
to make it work. It’s better to just get rid of eval() and find a better
way to do what you want to do.

Answered By: koukouviou

If you want:

def apply_op():
    x, y, z = [0.5, 0.25, 0.75]
    op = "x,y,z"
    return [eval(o) for o in op.split(",")]
print(apply_op())

to work you’ll need to capture the locals and globals as the issue is that eval(o) is the same has eval(o, globals(), locals()) but as the eval appears within the generator function the results of those functions aren’t the same as they were when the eval didn’t have a wrapping function so capture them outside the generator and use them inside:

def apply_op():
    x, y, z = [0.5, 0.25, 0.75]
    op = "x,y,z"
    _locals = locals()
    _globals = globals()
    return [eval(o, _globals, _locals) for o in op.split(",")]
print(apply_op())

Or better as x,y,z are locals and the strings are only variable names:

def apply_op():
    x, y, z = [0.5, 0.25, 0.75]
    op = "x,y,z"
    _locals = locals()
    return [_locals[o] for o in op.split(",")]
print(apply_op())
Answered By: Dan D.