exec / eval

eval and exec run Python code from a string at runtime — powerful for legitimate dynamic-execution use cases, and a serious security hazard the moment untrusted input reaches them.

eval() and exec() are built-in functions that take a string (or compiled code object) and run it as live Python code, using the interpreter itself as an execution engine at runtime. eval() evaluates a single EXPRESSION and returns its value; exec() runs arbitrary STATEMENTS (including multiple lines, loops, function/class definitions) and returns nothing. Both are legitimate, occasionally necessary tools — and both are also the single most common way Python programs accidentally become remotely exploitable, so this topic treats the security discussion as a first-class concern, not a footnote.

eval() vs exec(): Expressions vs Statements

eval(source) compiles and runs source as a single Python EXPRESSION and returns its resulting value — source can be "2 + 2" or "my_list[0]", but not "x = 5" (an assignment is a statement, not an expression, so eval rejects it with SyntaxError). exec(source) compiles and runs source as a sequence of Python STATEMENTS — assignments, loops, def, class, import, multiple lines — and always returns None; any value must be retrieved afterward from the namespace exec ran in.

result = eval("3 + 4 * 2")
print(result)  # 11

# exec runs statements and returns None; results come from side effects
namespace = {}
exec("x = 10\ny = x * 2", namespace)
print(namespace["y"])  # 20

try:
    eval("x = 5")   # SyntaxError -- assignment is a statement, not an expression
except SyntaxError as e:
    print("eval rejects statements:", e)

Namespaces: globals and locals Arguments

Both functions accept optional globals and locals dict arguments controlling what names the executed code can see and modify. Omitted, they default to the CALLER's actual global/local namespaces — meaning unrestricted eval/exec can read and mutate the calling scope's real variables. Passing an explicit (often nearly empty) dict isolates the executed code into its own sandboxed namespace instead, which is the first (though not sufficient on its own) step toward safer use.

Supplying an explicit namespace dict (here starting from {"__builtins__": {}}) keeps executed code from touching the calling module's real globals.

x = 100

# Without explicit namespaces: sees and can affect the real global 'x'
exec("x = 999")
print(x)  # 999 -- the module-level x was reassigned!

# With an isolated namespace dict: fully separate
sandbox = {"__builtins__": {}}
exec("x = 999", sandbox)
print(x)          # 100 -- untouched
print(sandbox["x"])  # 999 -- lives only in the isolated dict

Legitimate Use Cases

eval/exec have a handful of genuinely appropriate niches: interactive shells and REPLs (which must, by definition, run arbitrary code the user types); template engines and code generators that produce and run Python from trusted, internally-generated source; simple configuration/formula evaluators where the input is fully controlled by the application author, never end users; debugging tools that need to evaluate expressions against a live process's state; and certain testing/mocking frameworks. In essentially every case, the defining trait of a legitimate use is: the source string is either written by a developer or generated entirely by trusted, non-user-controlled logic.

{"__builtins__": {}} strips access to built-in functions (open, __import__, etc.), and the third argument supplies only an explicit allowlist of names — still not bulletproof (see Security Risks below), but far safer than a bare eval().

# A legitimate, narrow use: evaluating a mathematical formula the app author controls,
# with a restricted namespace exposing only what's needed.
import math

allowed_names = {"sqrt": math.sqrt, "pi": math.pi}

def evaluate_formula(formula: str) -> float:
    return eval(formula, {"__builtins__": {}}, allowed_names)

print(evaluate_formula("sqrt(16) + pi"))  # 7.141592653589793

Why Untrusted Input Must Never Be Evaluated

Passing any string influenced by an untrusted source (user input, request parameters, data from a file/network the user could tamper with) into eval() or exec() is a critical, well-documented security vulnerability class — arbitrary code execution (ACE). Because the string becomes real running Python code, an attacker can do anything Python itself can do: read/write files, open network connections, import modules, execute shell commands, exfiltrate secrets, or crash the process — full compromise of the running process's privileges, not merely 'a weird bug.'

This is illustrative, not something to execute — it demonstrates why any user-influenced string reaching eval()/exec() is treated as a critical vulnerability, regardless of how implausible the specific payload looks.

# NEVER do this with a user-supplied string:
user_input = "__import__('os').system('rm -rf /')"  # a hypothetical malicious payload
# eval(user_input)  # do not run -- illustrative only

Restricted Namespaces Are Not a Complete Sandbox

A common but dangerously incomplete mitigation is stripping __builtins__ and passing a small allowlist of names, as shown above. This blocks the most obvious attacks but is well known to be bypassable: Python's object model exposes ways to reach dangerous functionality indirectly through attribute chains on ordinary objects (e.g. walking from a string literal's type through __subclasses__() to find and instantiate classes that expose file or process operations), which a restricted-namespace eval does not prevent. There is no supported, fully secure way to eval/exec genuinely untrusted, adversarial Python source inside the same process — this is a well-established result in the Python security community, not a solvable configuration problem.

Real Safer Alternatives to eval/exec

For the tasks people often reach for eval/exec to solve, safer purpose-built tools almost always exist: ast.literal_eval() (standard library) safely evaluates a string containing only Python LITERALS (numbers, strings, tuples, lists, dicts, booleans, None) — no arbitrary code execution is possible, because it never runs code, it only parses a restricted literal grammar. For configuration, use json, configparser, tomllib (3.11+), or yaml (third-party). For simple math expressions, a small hand-written parser or a vetted third-party expression-evaluation library is far safer than restricted eval.

ast.literal_eval parses the string into an AST and only accepts a fixed grammar of literal syntax, rejecting anything that looks like a function call, attribute access, or other executable construct.

import ast

data = ast.literal_eval("[1, 2, {'a': True, 'b': None}]")
print(data)  # [1, 2, {'a': True, 'b': None}]

try:
    ast.literal_eval("__import__('os').system('echo hi')")
except ValueError as e:
    print("rejected:", e)  # not a literal -- ast.literal_eval refuses to run code

exec() and compile() for Code Generation Workflows

compile(source, filename, mode) compiles source text into a code object without running it, which exec/eval can then execute (mode='exec' for exec, mode='eval' for eval). Pre-compiling is useful when the same generated source runs many times (avoiding recompilation overhead) and lets you supply a meaningful filename string so tracebacks from the executed code point somewhere sensible instead of <string>.

source = "result = sum(i * i for i in range(5))"
code_obj = compile(source, "<generated>", "exec")

namespace = {}
exec(code_obj, namespace)
print(namespace["result"])  # 30

Edge Cases

exec() given only a globals dict (no separate locals) uses globals for both, which matters for name resolution inside function/class definitions created by the executed code. A bare eval("input()") still genuinely blocks on real stdin, because eval runs the expression with the real Python runtime behind it — it is not some restricted subset of the language, it is the language. Syntax errors and exceptions raised by the executed code propagate to the caller exactly as if that code had been written inline, so eval/exec calls should generally be wrapped in try/except when the source isn't 100% guaranteed well-formed.

Practical Usage

Outside of REPLs/debuggers and narrow, fully-trusted code-generation pipelines, reach for eval/exec rarely, and always ask first whether ast.literal_eval, a real parser, a config-file format, or a plugin/dispatch mechanism using a lookup dict instead of dynamic execution would solve the same problem more safely. A dispatch dict ({"add": add_fn, "sub": sub_fn}[operation](a, b)) replaces an astonishing number of 'eval a formula string' use cases without ever running arbitrary code.

# Instead of eval(f"{op}(a, b)"), use an explicit dispatch table:
operations = {
    "add": lambda a, b: a + b,
    "sub": lambda a, b: a - b,
}

def calculate(op: str, a: float, b: float) -> float:
    if op not in operations:
        raise ValueError(f"unsupported operation: {op}")
    return operations[op](a, b)

print(calculate("add", 3, 4))  # 7

Best Practices

Never pass a string derived, even indirectly, from user input to eval/exec — treat this as an absolute rule, not a judgment call to weigh case by case. When source truly must come only from the developer (config templates you author, internal tooling), still prefer ast.literal_eval for data and reserve exec for genuine code-generation needs. Always pass explicit globals/locals dicts rather than relying on the caller's real namespace, to limit blast radius even in trusted contexts. Log or audit any remaining eval/exec call sites in a codebase — they are exactly the places a security review should scrutinize first.

Key Takeaways

eval() runs a single expression and returns its value; exec() runs arbitrary statements and always returns None.

Both accept optional globals/locals dicts; omitted, they default to the caller's real namespace, which they can then read and mutate.

Legitimate uses are narrow: REPLs, trusted code generation, developer-authored formula/config evaluation — never on strings influenced by untrusted input.

Evaluating untrusted input is a critical arbitrary-code-execution vulnerability, not a minor bug — an attacker gains everything the running process can do.

Stripping __builtins__ and passing an allowlist namespace raises the bar but is a known-bypassable mitigation, not a real sandbox.

ast.literal_eval() safely parses Python literal syntax (numbers, strings, lists, dicts, etc.) without ever executing code — prefer it for data.

A dispatch dict or a real config-file format almost always replaces the need for eval/exec more safely.

formula = input("Enter a formula: ") result = eval(formula) # runs whatever the user typed as real Python code import ast formula = input("Enter a formula: ") result = ast.literal_eval(formula) # only parses literals -- rejects executable code eval() on raw user input is arbitrary code execution; ast.literal_eval() only accepts a restricted literal grammar (numbers, strings, lists, dicts, tuples, booleans, None) and never runs code, making it safe for data.

config_str = request.form["config"] # from a web request -- untrusted exec(config_str) # attacker-controlled code executes with full process privileges import json config_str = request.form["config"] config = json.loads(config_str) # parses data only, never executes code

Any string that originates from outside the application's own trusted source (web requests, user files, network responses) must never reach exec()/eval(); use a data format parser (json, tomllib, configparser) instead.

def safe_eval(expr): return eval(expr, {"__builtins__": {}}) # believed 'safe' because builtins are stripped import ast def safe_eval(expr): return ast.literal_eval(expr) # genuinely safe -- never executes code at all

Stripping __builtins__ is a well-known-bypassable mitigation, not a real sandbox (attribute-chain tricks can still reach dangerous functionality); only a tool that never executes code, like ast.literal_eval, is actually safe for untrusted data.

What is the core difference between what eval() and exec() are each allowed to run?

eval() runs a single Python EXPRESSION and returns its value; exec() runs arbitrary Python STATEMENTS (assignments, loops, def, class, multiple lines) and always returns None — results from exec must be retrieved from the namespace it ran in.

What happens if you omit the globals/locals arguments to eval/exec, and why does that matter for safety?

They default to the caller's actual global/local namespaces, meaning the executed code can read AND modify the calling scope's real variables — this is why explicit, isolated namespace dicts should always be supplied when the source isn't fully trusted top-level code.

Why is evaluating untrusted input with eval/exec considered a critical security vulnerability rather than just a bug?

Because the string becomes real running Python code with the full privileges of the process, an attacker can do anything Python itself can do — read/write files, open network connections, run shell commands, exfiltrate data — not merely produce an incorrect result.

Is stripping __builtins__ and passing an allowlist namespace to eval() a complete security sandbox? Why or why not?

No — it's a known-bypassable mitigation. Python's object model exposes indirect paths to dangerous functionality (e.g. via attribute chains and __subclasses__()), so a restricted-namespace eval() does not fully prevent arbitrary code execution against adversarial input.

What does ast.literal_eval() do differently from eval() that makes it safe for parsing untrusted data?

It parses the string into an AST and accepts ONLY a fixed grammar of Python literals (numbers, strings, lists, dicts, tuples, booleans, None) — it never compiles or executes arbitrary code, so there is no path to code execution regardless of the input.

What is compile(source, filename, mode) used for in relation to eval/exec?

It compiles source text into a reusable code object (without running it) that eval/exec can execute — useful when the same generated code runs repeatedly (avoids recompiling each time) and lets you supply a meaningful filename so tracebacks reference something other than <string>.

For a 'run one of several named operations' use case, what safer alternative replaces eval(f"{op_name}(a, b)")?

A dispatch dict mapping operation names to functions/callables (e.g. {"add": lambda a, b: a + b}[op_name](a, b)) — it achieves the same dynamic dispatch without ever executing a string as code.

Write a function safe_parse_literal(text: str) that uses ast.literal_eval to parse a string into a Python literal, returning None and printing an error message if the string isn't a valid literal.

import ast def safe_parse_literal(text: str): try: return ast.literal_eval(text) except (ValueError, SyntaxError): print(f"not a valid literal: {text!r}") return None print(safe_parse_literal("[1, 2, 3]")) # [1, 2, 3] print(safe_parse_literal("os.system('ls')")) # not a valid literal: ... ; None

ast.literal_eval raises ValueError (or SyntaxError for malformed syntax) when the string isn't a plain literal, including for anything resembling a function call or attribute access — catching both lets the function fail safely instead of ever executing code.

What does this print, and why doesn't the outer x change?

It prints 5 {'x': 6}. Passing an explicit globals dict ({"x": x}) isolates the executed code from the real module-level x; exec reads and modifies its own copy in that dict (and, since a separate locals_dict was given, writes land there instead of the globals dict), leaving the real outer x at its original value of 5.

Write a small formula calculator calculate(expression: str) using a dispatch-based safe evaluator (no eval/exec) that supports +, -, *, / between two numbers given as a 3-token string like '3 + 4'.

import operator OPS = {"+": operator.add, "-": operator.sub, "*": operator.mul, "/": operator.truediv} def calculate(expression: str) -> float: left_str, op, right_str = expression.split() if op not in OPS: raise ValueError(f"unsupported operator: {op}") return OPS[op](float(left_str), float(right_str)) print(calculate("3 + 4")) # 7.0 print(calculate("10 / 2")) # 5.0

Instead of building a string like f"{left} {op} {right}" and calling eval() on it, the operator is looked up in a dispatch dict of real functions from the operator module and called directly — no code is ever executed from a string, so untrusted input cannot escape the intended arithmetic.

x = 5
exec("x = x + 1", {"x": x}, locals_dict := {})
print(x, locals_dict)