Skip to content

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)

  1. eval vs exec
  2. Namespaces: where code runs
  3. Building functions dynamically
  4. eval for math DSLs (controlled)
  5. ast.literal_eval — the safe subset
  6. compile() — the third sibling
  7. Security model (honest assessment)
  8. Legitimate use cases
  9. Common bugs
  10. Python 3.10+ notes
  11. 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:

  1. Don't — use a declarative DSL parsed with ast whitelist
  2. Subprocess with seccomp / container / timeout
  3. 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 SyntaxError in exec/eval strings
  • Pattern matching (match) can appear in exec strings on 3.10+
  • Prefer ast.Constant over deprecated ast.Num / ast.Str when 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#

  1. What is the difference between eval and exec in accepted input and return value?
  2. Why is ast.literal_eval safer than eval?
  3. What does an explicit namespace dict protect against (and what does it not protect against)?
  4. When should you use compile() before exec?
  5. 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