Code Generation using exec() and eval()#
Python can execute and evaluate strings as code at runtime via exec() and eval(). Both are full metaprogramming escape hatches: indispensable in REPLs, calculators, and tightly sandboxed DSLs — and dangerous whenever input crosses a trust boundary.
How to use this page
Read with type() for Dynamic Class Creation and Monkey Patching. For safer configuration, prefer JSON/YAML + schema validation. Security context: Managing Secrets.
At a glance
| Track | Python Advanced → Meta Programming |
| Sections | 11 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: eval vs exec · Namespaces: where code runs · Building functions dynamically · eval for math DSLs (controlled) · ast.literal_eval — the safe subset · compile() — the third sibling · Security model (honest assessment) · Legitimate use cases · … (+3 more)
evalvsexec- Namespaces: where code runs
- Building functions dynamically
evalfor math DSLs (controlled)ast.literal_eval— the safe subsetcompile()— the third sibling- Security model (honest assessment)
- Legitimate use cases
- Common bugs
- Python 3.10+ notes
- Alternatives decision table
eval vs exec#
eval(expression, globals?, locals?) |
exec(code, globals?, locals?) |
|
|---|---|---|
| Accepts | Single expression | Statements, blocks, compound code |
| Returns | Value of the expression | None |
| Typical use | Calculator, simple template math | Define functions/classes dynamically |
| Risk level | High | Higher (imports, loops, assignments) |
x = eval("2 + 3 * 4")
# x == 14
result = eval("[n**2 for n in range(5)]")
# [0, 1, 4, 9, 16]
exec("y = 10")
# y == 10 — binds in current/local namespace
exec("def f(n):\n return n * 2")
f(21) # 42 — if f visible in namespace used by exec
Namespaces: where code runs#
Both functions accept optional globals and locals dicts. If locals is omitted, it aliases globals. If both omitted, they default to the calling scope (with nuances in nested scopes).
namespace: dict = {}
exec("counter = 0", namespace)
exec("counter += 1", namespace)
namespace["counter"] # 1
eval("counter", namespace) # 1
Isolate dynamic code
Always pass an explicit dict namespace — never exec(user_code) with default scopes in production.
safe_globals = {"__builtins__": {}} # strip builtins — still not a true sandbox
safe_locals: dict = {}
# eval("open('/etc/passwd')", safe_globals, safe_locals) # NameError — no open
Building functions dynamically#
source = """
def area(width, height):
return width * height
"""
namespace: dict = {}
exec(source, namespace)
area = namespace["area"]
area(3, 4) # 12
Compare with type() for Dynamic Class Creation — building a namespace dict yourself is safer than concatenating class source strings.
eval for math DSLs (controlled)#
import ast
import operator as op
# Whitelist of allowed AST node types and operators
_ALLOWED_OPS = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.USub: op.neg,
ast.Pow: op.pow,
}
def safe_eval(expr: str) -> float:
def _eval(node):
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp):
left = _eval(node.left)
right = _eval(node.right)
return _ALLOWED_OPS[type(node.op)](left, right)
if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub):
return _ALLOWED_OPS[ast.USub](_eval(node.operand))
raise ValueError(f"disallowed expression: {type(node).__name__}")
tree = ast.parse(expr, mode="eval")
return _eval(tree.body)
safe_eval("(2 + 3) * 4") # 20.0
# safe_eval("__import__('os').system('rm -rf /')") # ValueError
Never use bare eval(user_input)
eval("__import__('subprocess').call(['sh'])") succeeds with default builtins. One line of user input = arbitrary code execution.
ast.literal_eval — the safe subset#
For parsing Python literals only (strings, numbers, tuples, lists, dicts, booleans, None):
import ast
data = ast.literal_eval('{"name": "Ada", "scores": [10, 20]}')
# type: dict — no code execution
# ast.literal_eval("__import__('os')") # ValueError
| Tool | Executes code? |
|---|---|
eval |
Yes |
exec |
Yes |
ast.literal_eval |
No — data only |
json.loads |
No — JSON subset |
Default choice for config strings: json.loads or ast.literal_eval.
compile() — the third sibling#
code = compile("x + 1", "<string>", "eval")
x = 10
eval(code) # 11
func_code = compile("def f(): return 42", "<string>", "exec")
ns: dict = {}
exec(func_code, ns)
ns["f"]() # 42
| Mode | compile mode flag |
|---|---|
| Single expression | "eval" |
| Sequence of statements | "exec" |
ast module interaction |
"single" (REPL-style) |
compile + repeated exec/eval amortizes parsing cost when the same code runs many times.
Security model (honest assessment)#
There is no perfect in-process sandbox
Removing __builtins__ blocks naive attacks but determined code can escape via ().__class__.__bases__[0].__subclasses__() chains, frame objects, and C-extension side channels. Do not rely on namespace tricks for security boundaries.
| Threat | Bare eval/exec |
Hardened approach |
|---|---|---|
| File/network access | Trivial via imports | Separate process, OS sandbox, WASM |
| CPU exhaustion | Infinite loops in exec |
Timeout, resource limits (subprocess) |
| Data exfiltration | Full memory access | Never run untrusted code in-process |
Production patterns for untrusted code:
- Don't — use a declarative DSL parsed with
astwhitelist - Subprocess with seccomp / container / timeout
- External sandbox products (Pyodide, RestrictedPython with eyes open)
Legitimate use cases#
| Use case | Tool |
|---|---|
| REPL / interactive shell | exec, compile |
pandas.DataFrame.query(expr) |
eval with restricted env |
| Template engines (limited) | Custom ast walker |
| Test fixtures generating code | exec in isolated dict |
| ORM filter DSL | Parse → AST → SQL, not raw eval |
# pandas-style pattern: curated namespace
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
df.query("a > 1 and b < 6") # eval internally with restricted scope
Common bugs#
# Bug: expecting return from exec
result = exec("1 + 2") # result is None
# Fix: use eval for expressions
result = eval("1 + 2")
# Bug: exec in function doesn't leak to outer scope (Python 3)
def load():
exec("z = 99")
load()
# z # NameError — exec used local dict of load()
# Fix: pass explicit namespace and read from it
ns: dict = {}
exec("z = 99", ns)
ns["z"] # 99
Python 3.10+ notes#
- Better error messages for
SyntaxErrorinexec/evalstrings - Pattern matching (
match) can appear inexecstrings on 3.10+ - Prefer
ast.Constantover deprecatedast.Num/ast.Strwhen walking ASTs
# 3.10+ structural pattern matching via exec
exec("""
def classify(x):
match x:
case int() if x > 0:
return 'positive'
case _:
return 'other'
""")
Alternatives decision table#
| Need | Avoid | Use instead |
|---|---|---|
| User math formulas | eval |
ast whitelist or numexpr |
| Config files | exec |
JSON, TOML, YAML + validation |
| Plugin behavior | exec of uploaded code |
Entry points, importlib metadata |
| Dynamic class shape | exec class strings |
type(), dataclasses.make_dataclass |
| Template variables | eval |
string.Template, Jinja2 (autoescape) |
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
eval(request.form["expr"]) |
Remote code execution | ast.literal_eval or parser |
Empty __builtins__ = safe |
Escapes still exist | Process isolation |
exec returns value |
Always None |
Use eval or namespace dict |
eval only for numbers |
Strings invoke repr, attacks | Whitelist AST nodes |
| Logging generated code | Secrets in strings | Redact, don't log user code |
Mental model checklist#
- What is the difference between
evalandexecin accepted input and return value? - Why is
ast.literal_evalsafer thaneval? - What does an explicit namespace dict protect against (and what does it not protect against)?
- When should you use
compile()beforeexec? - What is the correct architecture for running untrusted user code?
What's next#
| Topic | Page |
|---|---|
| Dynamic classes | type() for Dynamic Class Creation |
| Metaclasses | Metaclasses |
| Monkey patching | Monkey Patching |
| Introspection | Inspect Module for Introspection |
| Secrets & security | Managing Secrets |