Search code examples
pythonabstract-syntax-tree

Python AST exec "... is not defined" error on recursive function


I came across this error

def test_rec():
    import ast
    exec(compile(ast.fix_missing_locations(ast.parse("""
def fact(n):
    return 1 if n == 0 else n * fact(n - 1)

print(fact(5))
"""), "<string>", "exec")))

This yield this error, which is weird

Traceback (most recent call last):
  File "/Users/gecko/.pyenv/versions/3.9.0/envs/lampy/lib/python3.9/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/Users/gecko/code/lampycode/tests/test_let_lang.py", line 6, in test_rec
    exec(compile(ast.fix_missing_locations(ast.parse("""
  File "<string>", line 4, in <module>
  File "<string>", line 3, in fact
NameError: name 'fact' is not defined

If I copy and paste the same code in REPL it works fine

>>> def fact(n):
...     return 1 if n == 0 else n * fact(n - 1)
... 
>>> print(fact(5))
120
>>>

Any ideas?

I could reduce the problem further here is the minimal exempla, this would overflow the stack but it gives me the same not defined error

def test_rec3():
    exec("""
def f():
    f()

f()
""")

--

Second edit, going even further, this only happens inside functions

This works

exec("""
def f(n):
    print("end") if n == 1 else f(n-1)

f(10)""")

But this gives me the same error as above

def foo():
    exec("""
def f(n):
    print("end") if n == 1 else f(n-1)

f(10)""")


foo()

Solution

  • If you use exec with the default locals, then binding local variables is undefined behavior. That includes def, which binds the new function to a local variable.

    Also, functions defined inside exec can't access closure variables, which fact would be.

    The best way to avoid these problems is to not use exec. The second best way is to provide an explicit namespace:

    namespace = {}
    exec(whatever, namespace)