70.3 exec() and eval(): Dynamic Code Execution
Right, let’s talk about exec() and eval(). These are the two functions that make Python programmers either feel like omnipotent wizards or get them instantly fired. They allow you to take a string of code and run it on the fly, dynamically. It’s the programming equivalent of handing a loaded script to your interpreter and saying, “Here, run this, I dare you.”
The core difference between them is often muddled but is actually quite simple:
eval()is for evaluation. It takes a string containing a Python expression, evaluates it, and returns the result. Think of it as a calculator function.exec()is for execution. It takes a string containing arbitrary Python code (e.g., loops, class definitions, multi-line nonsense), executes it, and returnsNone. Think of it as a mini-interpreter.
Using them is deceptively straightforward, which is precisely where the trouble starts.
The Basic Incantations
Here’s eval() in its natural habitat: turning a string into a live value.
# A simple expression
result = eval("2 + 2 * 3")
print(result) # Output: 8
# Using a variable from the current scope
x = 10
result = eval("x * 5")
print(result) # Output: 50
And here’s exec(), which is less about getting a value and more about making things happen.
# Executing a block of code
code_string = """
for i in range(3):
print(f"Hello from exec, iteration {i}")
"""
exec(code_string)
# Output:
# Hello from exec, iteration 0
# Hello from exec, iteration 1
# Hello from exec, iteration 2
# Defining a function dynamically
function_string = """
def dynamically_created_greeting(name):
return f"Ah, {name}! I've been expecting you."
"""
exec(function_string)
# Now the function exists in our current scope!
greeting = dynamically_created_greeting("Alice")
print(greeting) # Output: Ah, Alice! I've been expecting you.
The Scoping Maze (This is Where They Get You)
This is the part the manual often glosses over, and it’s the source of 90% of the head-scratching. The scoping behavior of exec() and eval() changes depending on whether you use the optional globals and locals dictionaries. If you don’t supply them, they run in the current scope. This is almost always a catastrophically bad idea.
x = 10 # A global variable
def test_scope():
y = 20 # A local variable
# This works, it can see the global 'x'
result1 = eval("x + 1")
# This will FAIL with a NameError. Why?
# Because the eval is happening inside this function, and it can't see 'y'.
result2 = eval("y + 1")
test_scope()
The solution, and a critical best practice, is to explicitly control the context by passing dictionaries.
x = 10
def test_safer_scope():
y = 20
# Create a custom globals and locals dictionary.
# We can choose what variables to include.
custom_globals = {'x': x} # We explicitly pass in 'x'
custom_locals = {'y': y} # We explicitly pass in 'y'
# Now it works, because we've explicitly provided 'y'
result = eval("y + 1", custom_globals, custom_locals)
print(result) # Output: 21
# But watch this: we provided a SEPARATE 'x' in globals.
# This does NOT affect the real global 'x'.
exec("x = 999", custom_globals, custom_locals)
print(custom_globals['x']) # Output: 999 (the copy we passed in was changed)
print(x) # Output: 10 (the real global is untouched)
test_safer_scope()
This explicit passing is your first and best line of defense. It sandboxes the dynamic code, preventing it from accidentally clobbering your actual variables.
Why You Should Be Terrified (And When To Use Them Anyway)
Let’s be direct: blindly eval()ing or exec()ing a string from an untrusted source (like a user, a network request, a config file you didn’t write) is like granting a random stranger root access to your server. It’s an instant, gaping security hole. They can import os and rm -rf / your entire project, or worse.
So, when is it okay?
- You are the source. The string is generated by your own code for a specific purpose (e.g., code generators, templating engines you built).
- It’s a closed system. You’re building a developer tool (like a debugger or a REPL) where the user is expected to input code.
- It’s for configuration, but only under extreme duress. Sometimes, a configuration value truly needs to be an expression. If you must, you use heavily restricted globals (
{'__builtins__': {}}) to neuter the built-in functions and modules, making it much safer. But you’d better have a darn good reason.
The AST: Your Sanity Check
Before you reach for exec, ask yourself if you can achieve the same goal by working with the Abstract Syntax Tree (AST). The ast module lets you parse, analyze, and even modify code structures safely. You can walk the tree, validate that it only contains operations you allow (e.g., no imports, no calls to os), and then compile it to bytecode. It’s more work, but it transforms a reckless security gamble into a controlled, auditable process. It’s the difference between handing someone a live grenade and carefully disassembling it on a workbench to see how it ticks.
In summary, exec and eval are incredibly powerful tools that reside right on the edge of “brilliant” and “insanely stupid.” Respect them, contain them with explicit scoping dictionaries, and always—always—prefer the safer alternative of the ast module if you’re dealing with anything even remotely questionable. Your future self, who isn’t debugging a production server breach, will thank you.