Introduction
Python ships with two powerful built-in functions -- eval() and exec() -- that let you execute code stored as strings at runtime. Understanding when and how to use them safely is essential for any developer building plugin systems, configuration-driven logic, or template engines, while misusing them is one of the fastest ways to introduce critical security vulnerabilities.
Key Concepts
eval(): Evaluates a single Python expression and returns its result.exec(): Executes an arbitrary block of Python statements and always returnsNone.- Namespace restriction: The practice of passing custom
globalsandlocalsdictionaries to limit what names are accessible during dynamic execution. - Arbitrary code execution (ACE): A security vulnerability where an attacker can run any code on your system, often introduced by passing untrusted input to
eval()orexec(). ast.literal_eval(): A safe alternative that only evaluates Python literal structures (strings, numbers, tuples, lists, dicts, booleans, andNone).
Real World Context
Imagine you are building a reporting tool that lets users type simple math formulas like revenue - costs into a configuration file. You could use eval() to compute the result dynamically. However, if you pipe raw user input straight into eval(), a malicious user could wipe your server. Knowing how to sandbox these functions -- and when to avoid them entirely -- is a day-one production skill.
Deep Dive
eval() -- Evaluate a Single Expression
The signature is eval(expression, globals=None, locals=None). It parses and evaluates a single Python expression, returning its value. The following example shows basic arithmetic, variable lookup, and custom namespace usage:
pythonresult = eval("2 + 3 * 4") print(result) # 14 # With variables x = 10 print(eval("x ** 2")) # 100 # With custom namespace print(eval("a + b", {"a": 5, "b": 7})) # 12
Notice that each call returns the computed value, making eval() ideal for dynamic formula evaluation. However, eval() only handles expressions -- things that produce a value. It cannot execute statements like if, for, def, class, or assignments. Attempting to do so raises a SyntaxError:
pythoneval("x = 5") # SyntaxError: invalid syntax
This limitation is by design: eval() is intentionally narrow so you know exactly what it can do.
exec() -- Execute Arbitrary Statements
When you need to run full Python code -- function definitions, class declarations, loops -- you reach for exec(code, globals=None, locals=None). Unlike eval(), it always returns None; any results must be captured through the namespace dictionaries you pass in. Here is an example that defines both a function and a class dynamically:
pythonnamespace = {} exec(""" def greet(name): return f'Hello, {name}!' class Point: def __init__(self, x, y): self.x = x self.y = y """, namespace) print(namespace["greet"]("Alice")) # Hello, Alice! p = namespace["Point"](1, 2) print(p.x, p.y) # 1 2
After execution, the namespace dictionary contains the greet function and the Point class, ready to be used like any other Python object.
The Dangers of Untrusted Input
You should never run exec() or eval() on untrusted input. Doing so opens the door to arbitrary code execution, where an attacker can run any system command through Python. Consider this example of what a malicious user could supply:
python# A malicious user could input: eval("__import__('os').system('rm -rf /')")
This single line could destroy an entire filesystem. The risk applies equally to exec().
Restricting the Scope
You can reduce the attack surface by passing custom globals and locals dictionaries that strip out dangerous builtins. The following example shows how to create a restricted evaluation environment:
python# Remove all builtins result = eval("2 + 2", {"__builtins__": {}}) # Allow only specific names safe_globals = {"__builtins__": {}, "sum": sum, "range": range} result = eval("sum(range(10))", safe_globals) # 45
While this is a useful first layer of defense, be aware that even with {"__builtins__": {}}, determined attackers can escape via introspection on built-in types. For truly safe evaluation of untrusted input, use ast.literal_eval() (which only parses literals) or a dedicated sandboxing library.
eval() vs exec() at a Glance
The following table summarizes the key differences between the two functions:
| Feature | eval() | exec() |
|---|---|---|
| Input | Single expression | Any code (statements, blocks) |
| Returns | The expression's value | None |
| Can define functions/classes | No | Yes |
| Can do assignments | No | Yes |
Choose eval() when you need a return value from a simple expression, and exec() when you need to execute full statements.
Common Pitfalls
- Using
eval()whereast.literal_eval()suffices. If you only need to parse a Python literal (e.g., a list or dict from a config file),ast.literal_eval()is far safer because it refuses to execute arbitrary expressions. - Assuming
{"__builtins__": {}}is a complete sandbox. Attackers can still reach dangerous functions through object introspection chains like().__class__.__bases__[0].__subclasses__(). Namespace restriction reduces risk but does not eliminate it. - Forgetting that
exec()returnsNone. A common mistake is writingresult = exec("x = 5")and expectingresultto be5. You must retrieve values from the namespace dictionary instead.
Best Practices
- Default to not using
eval()/exec()at all. In most cases, safer alternatives exist -- dictionaries for dispatch,getattr()for dynamic attribute access, orast.literal_eval()for parsing literals. - Always restrict the namespace when dynamic execution is genuinely necessary. Pass explicit
globalsandlocalsdictionaries containing only the names the code needs. - Log and audit all dynamically executed code in production systems. If something goes wrong, you need a trail showing exactly what code was run and where it came from.
Summary
eval()evaluates a single expression and returns its value;exec()executes arbitrary statements and returnsNone.- Both functions accept optional
globalsandlocalsdictionaries to restrict the available namespace. - Running
eval()orexec()on untrusted input is a critical security vulnerability -- always validate or sandbox input first. ast.literal_eval()is the safe choice for parsing Python literals from external sources.- When you must use dynamic execution, restrict builtins, whitelist allowed names, and audit every invocation.
Code Examples
# Safe(r) evaluation with restricted builtins
allowed_names = {"sum": sum, "range": range, "len": len}
code = "sum(range(5))"
result = eval(code, {"__builtins__": {}}, allowed_names)
print(result) # 10
# exec() to define a function dynamically
namespace = {}
exec("""
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
""", namespace)
print(namespace["factorial"](5)) # 120