eval, exec, and compile
eval, exec, and compile
Three builtins form the dynamic-execution toolkit:
code = compile("1 + 2", "<calc>", "eval") # → code object; modes: eval/exec/single
eval(code) # expressions only → returns 3
eval("x + 1", {"x": 10}) # globals dict supplies names
exec("y = 5") # statements → returns None, side effects only
d = {}
exec("y = 5", d); d["y"] # 5 — result lands in the dict
eval demands an expression and yields its value; exec accepts statements (defs, loops, imports) and yields None. compile separates parsing from execution — precompile hot snippets once, then eval the code object repeatedly. eval also accepts an exec-mode code object only through exec; the reverse fails with a SyntaxError.
Scope Inside Functions
Scope Inside Functions
exec inside a function cannot write local variables — the compiler decided the function’s fast locals before the string ran:
def f():
exec("a = 1") # a does NOT appear here
return locals().get("a") # depends on the dict passed in
def g():
scope = {}
exec("a = 1", scope) # explicit dict = reliable channel
return scope["a"]
Pass a single dict as both globals and locals and assignments become visible to code sharing that dict:
scope = {"__builtins__": {}}
exec("result = sum(values)", scope) # scope["result"] after run
Two dicts (globals separate from locals) send assignments only to the locals mapping — reads still see globals, which produces confusing partial environments. Prefer explicit data flow over exec-driven name injection.
Security and Safe Alternatives
Security and Safe Alternatives
Never evaluate untrusted input. There is no reliable sandbox: even with {"__builtins__": {}}, reachable objects lead to subclasses of object and then to ().__class__.__mro__[1].__subclasses__(), which exposes file and process primitives. Every "restricted eval" has been escaped.
Safe substitutions:
| Need | Use |
|---|---|
| Parse literals (numbers, lists, dicts) | ast.literal_eval(s) — accepts only literal syntax |
| Data interchange | json.loads |
| Config expressions | A purpose-built grammar (e.g. ast walk with an allowlist) |
| Plugins/callables | importlib over vetted names, not eval(f"{name}()") |
Interview angle: say plainly that eval executes arbitrary code with the caller’s privileges, show ast.literal_eval as the narrow exception, and name one real-world incident class (template/config injection).
Quiz
1. What does exec("x = 1") return?
2. Why doesn’t exec("a = 1") define a local in an enclosing function?
3. Safest way to parse a JSON-looking string from untrusted input?
Flashcards
Question
eval vs exec
Click to reveal answer
Answer
eval = expression → value; exec = statements → None with side effects.
Question
compile modes
Click to reveal answer
Answer
"eval" for expressions, "exec" for statements, "single" for interactive lines — returns a code object.
Question
exec scope rule
Click to reveal answer
Answer
Writes go into the dict you pass; function locals are unreachable without an explicit scope dict.
Question
ast.literal_eval
Click to reveal answer
Answer
Parses literals (str/num/list/dict/tuple/bool/None) safely — the sanctioned eval replacement.
Revision Notes
Key Takeaways
- 1. eval/exec/compile separate parsing from execution but grant full interpreter power
- 2. Function-scope dynamics require an explicit dict; never rely on exec to create locals
- 3. There is no safe eval for untrusted input — use literal_eval, json, or a restricted grammar
Interview Tips
- • Give the sandbox-escape reasoning (__subclasses__ chain) when asked why eval is unsafe
- • Show the two-dict vs one-dict scope distinction if pressed on dynamic execution details
Cheat Sheet
eval(expr)→value; exec(stmts)→None; compile(src, "", "eval"|"exec"); exec writes only passed dicts; no eval sandbox → ast.literal_eval.