Python - eval() and exec() Functions
Python's dynamic nature gives you powerful tools for runtime code execution. Two of the most potent—and dangerous—are `eval()` and `exec()`. These built-in functions let you execute Python code...
Key Insights
eval()evaluates single expressions and returns results, whileexec()executes arbitrary code blocks without returning values—understanding this distinction is fundamental to using them correctly.- Never use
eval()orexec()with untrusted input; sandboxing attempts are consistently bypassed by determined attackers, making these functions inherently dangerous for user-facing applications. - For most use cases involving data parsing,
ast.literal_eval()provides a safe alternative that handles Python literals without executing arbitrary code.
Introduction
Python’s dynamic nature gives you powerful tools for runtime code execution. Two of the most potent—and dangerous—are eval() and exec(). These built-in functions let you execute Python code constructed as strings, enabling metaprogramming patterns that would be impossible in statically compiled languages.
The distinction is straightforward: eval() evaluates a single expression and returns its value. exec() executes arbitrary statements but returns nothing. Think of eval() as a calculator and exec() as an interpreter.
You might reach for these functions when building REPLs, configuration systems that allow computed values, or plugin architectures. But before you do, understand what you’re getting into. These functions are loaded guns pointed at your application’s security.
Understanding eval()
The eval() function takes a string containing a Python expression, parses it, and returns the computed result. Its signature is:
eval(expression, globals=None, locals=None)
At its simplest, eval() works like a calculator:
result = eval("2 + 3 * 4")
print(result) # 14
result = eval("len('hello')")
print(result) # 5
The power comes from evaluating expressions that reference variables. By default, eval() has access to the current scope:
x = 10
y = 20
result = eval("x + y")
print(result) # 30
You can control the execution environment by passing custom namespace dictionaries:
# Custom namespace with specific variables
namespace = {"a": 5, "b": 3}
result = eval("a ** b", namespace)
print(result) # 125
# The expression can't access variables outside the namespace
x = 100
result = eval("a + b", namespace) # Works: 8
# eval("x + a", namespace) # NameError: name 'x' is not defined
You can also make functions available:
import math
safe_functions = {
"sqrt": math.sqrt,
"abs": abs,
"round": round
}
result = eval("sqrt(abs(-16))", {"__builtins__": {}}, safe_functions)
print(result) # 4.0
Understanding exec()
While eval() handles expressions, exec() executes statements—assignments, loops, function definitions, imports, and multi-line code blocks. It never returns a value (technically returns None).
exec(object, globals=None, locals=None)
Here’s exec() running multi-line code:
code = """
total = 0
for i in range(5):
total += i * 2
print(f"Total: {total}")
"""
exec(code) # Output: Total: 20
You can dynamically define functions and classes:
function_code = """
def greet(name):
return f"Hello, {name}!"
"""
namespace = {}
exec(function_code, namespace)
# The function now exists in the namespace
greet = namespace["greet"]
print(greet("World")) # Hello, World!
Creating classes dynamically:
class_code = """
class Calculator:
def __init__(self, value=0):
self.value = value
def add(self, n):
self.value += n
return self
def result(self):
return self.value
"""
namespace = {}
exec(class_code, namespace)
Calculator = namespace["Calculator"]
calc = Calculator(10)
print(calc.add(5).add(3).result()) # 18
The globals and locals Parameters
Both functions accept globals and locals dictionaries that control what names are available during execution. This is your primary mechanism for namespace isolation.
When you pass a custom globals dictionary, it becomes the global namespace for the executed code. If it doesn’t contain __builtins__, Python adds the standard built-ins automatically:
# This still has access to all built-ins
namespace = {"x": 10}
eval("__import__('os').getcwd()", namespace) # Works!
To restrict built-ins, explicitly set __builtins__ to an empty dictionary or a limited set:
# Restricted environment - no built-ins available
restricted = {"__builtins__": {}, "x": 10, "y": 20}
result = eval("x + y", restricted)
print(result) # 30
# eval("len('test')", restricted) # NameError: name 'len' is not defined
Capturing variables created by exec() requires understanding how the namespace dictionaries work:
global_ns = {"__builtins__": __builtins__}
local_ns = {}
exec("""
result = 42
data = [1, 2, 3]
""", global_ns, local_ns)
print(local_ns) # {'result': 42, 'data': [1, 2, 3]}
When globals and locals are the same dictionary, modifications appear in that single namespace:
namespace = {}
exec("x = 10", namespace, namespace)
print(namespace["x"]) # 10
Security Risks and Dangers
Here’s where I need to be blunt: eval() and exec() with untrusted input are security vulnerabilities. Period. No amount of sandboxing makes them safe.
Consider this seemingly innocent calculator:
# DANGEROUS - Never do this!
user_input = input("Enter calculation: ")
result = eval(user_input)
A malicious user could enter:
# Malicious input examples
"__import__('os').system('rm -rf /')"
"__import__('subprocess').call(['cat', '/etc/passwd'])"
"open('/etc/passwd').read()"
“But I removed __builtins__!” you say. Attackers bypass this routinely:
# Bypassing __builtins__ = {} restriction
malicious = """
().__class__.__bases__[0].__subclasses__()[104].__init__.__globals__['sys'].modules['os'].system('whoami')
"""
# The index varies by Python version, but the technique works
This exploit walks the object hierarchy to find a class that has os in its globals. There are dozens of variations. Security researchers consistently find new bypasses.
Even attribute access restrictions fail:
# Attempting to block dangerous attributes
def safe_eval(expr):
if any(word in expr for word in ["import", "eval", "exec", "open"]):
raise ValueError("Forbidden!")
return eval(expr)
# Bypass using string concatenation
safe_eval("getattr(__builtins__, 'ev' + 'al')('1+1')")
The fundamental problem: Python’s introspection capabilities are too powerful. Any object can lead to dangerous functionality through attribute chains.
Safer Alternatives
For parsing Python literals (strings, numbers, lists, dicts, tuples, booleans, None), use ast.literal_eval():
import ast
# Safe parsing of data structures
data = ast.literal_eval("{'name': 'Alice', 'scores': [95, 87, 92]}")
print(data) # {'name': 'Alice', 'scores': [95, 87, 92]}
# Handles nested structures
config = ast.literal_eval("""
{
'debug': True,
'servers': ['localhost', '192.168.1.1'],
'timeout': 30
}
""")
# Rejects anything that isn't a literal
try:
ast.literal_eval("__import__('os')")
except ValueError as e:
print("Blocked!") # This is safe
For mathematical expressions, build a proper parser using ast:
import ast
import operator
class SafeEvaluator(ast.NodeVisitor):
"""Evaluates simple arithmetic expressions safely."""
OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
}
def visit_BinOp(self, node):
left = self.visit(node.left)
right = self.visit(node.right)
op = self.OPERATORS.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {node.op}")
return op(left, right)
def visit_UnaryOp(self, node):
operand = self.visit(node.operand)
op = self.OPERATORS.get(type(node.op))
if op is None:
raise ValueError(f"Unsupported operator: {node.op}")
return op(operand)
def visit_Num(self, node): # Python 3.7 and earlier
return node.n
def visit_Constant(self, node): # Python 3.8+
if isinstance(node.value, (int, float)):
return node.value
raise ValueError(f"Unsupported constant: {node.value}")
def generic_visit(self, node):
raise ValueError(f"Unsupported syntax: {type(node).__name__}")
def safe_math_eval(expression):
tree = ast.parse(expression, mode='eval')
return SafeEvaluator().visit(tree.body)
# Usage
print(safe_math_eval("2 + 3 * 4")) # 14
print(safe_math_eval("(10 - 3) ** 2")) # 49
# safe_math_eval("__import__('os')") # ValueError: Unsupported syntax
Legitimate uses for eval() and exec() do exist: Python REPLs and debuggers, code generation tools, certain testing frameworks, and plugin systems where code comes from trusted sources. In these cases, the code being executed is either from developers or from a controlled, trusted environment.
Conclusion
eval() and exec() are powerful tools that exist for good reasons. Python’s interactive interpreter uses them. Frameworks like Django’s ORM and Jinja2’s templating engine use them internally. They enable metaprogramming patterns that make Python remarkably flexible.
But they’re not for processing user input. Ever. The attack surface is too large, the bypass techniques too numerous, and the consequences too severe. If you’re tempted to use them with external data, stop and find another approach.
Use ast.literal_eval() for parsing data. Build proper parsers for domain-specific languages. Use established libraries for configuration and templating. Reserve eval() and exec() for development tools and trusted code execution only.