eval and exec let you run Python source code stored in a string at runtime. That sounds powerful, and it is, but it's also one of the most common ways to introduce a remote code execution vulnerability into a program. This lesson covers what each function does, why "sandboxing" them is not the safety net most people think it is, when to reach for ast.literal_eval instead, and the small handful of cases where eval or exec are genuinely the right tool.
eval Doeseval takes a single Python expression as a string, evaluates it, and returns the result. Expressions are the things that produce a value: arithmetic, function calls, comparisons, lookups. Statements like if, for, def, or assignment are not expressions and won't work in eval.
The string "2 + 3 * 4" is parsed as a Python expression, evaluated, and the integer 14 comes back. You can reference variables that exist in the surrounding scope.
eval saw the names price and discount_pct in the enclosing scope and looked them up. The full signature is eval(expression, globals=None, locals=None), where you can pass dictionaries to override which names eval can see. That knob is where most "secure eval" attempts go wrong.
If you hand eval something that isn't an expression, it raises SyntaxError.
Assignment is a statement, not an expression, so eval rejects it. For statements, you need exec.
exec Doesexec runs one or more Python statements from a string. It does not return a value. Whatever the code produces stays inside the namespace exec ran against.
Two things to notice. First, exec prints from inside the string because the print call ran. Second, exec itself returned None, even though the code set variables. If you want to keep those variables around, give exec an explicit namespace dictionary and read from it afterward.
The namespace dictionary is now the place where the executed code's variables live. You can pull them out by key. Note that exec also adds a __builtins__ entry to that dictionary automatically; that's normal.
Like eval, exec accepts globals and locals arguments. The signature is exec(code, globals=None, locals=None). When both default to None, the code runs in the current scope, which is rarely what you want and a frequent source of confusion when exec-defined variables don't appear inside functions.
Cost: Both eval and exec parse and compile the source string every single call. If you run the same string in a loop, that compile cost dominates. Use compile() to do the work once.
compileWhen eval or exec runs, Python parses the source string and turns it into a code object before executing it. The parsing and compilation are the slow part. For one-shot calls it doesn't matter. For hot paths, it adds up fast.
compile(source, filename, mode) does that work once and gives you a code object you can hand to eval or exec repeatedly.
| Mode | What it accepts | Used with |
|---|---|---|
"eval" | A single expression | eval |
"exec" | One or more statements | exec |
"single" | A single interactive statement | exec, REPL-like behavior |
The filename argument is purely a label that shows up in tracebacks. Pass something descriptive like "<discount-rule>" so errors are traceable.
Roughly a 5x speedup on this tiny expression, and the gap widens for longer code. If you're running the same template repeatedly (for example, applying a customer-defined formula to every row of an order table), compile it once outside the loop and reuse the code object inside.
Cost: compile() is not free either. Compiling once and reusing thousands of times is a clear win. Compiling fresh on every call is the same cost as calling eval on the string.
Here is the part you cannot skip. If any part of the string passed to eval or exec comes from outside your program (a user form, a config file, a network request, even an environment variable a less-trusted process might touch), you have a remote code execution vulnerability.
A naive discount-rule evaluator looks innocent.
Then someone passes in a different "rule".
That string is also a valid Python expression. It imports the os module and runs a shell command. If the program runs with permission to delete the orders directory, the orders directory is gone. The same trick can read environment variables, exfiltrate secrets, open network connections, or anything else the process can do.
It doesn't matter how short the input looks or how harmless the surrounding feature seems. An expression is an expression. Python will happily evaluate any of it.
The standard suggestion is "just restrict the globals and locals you pass in". The idea is to hide builtins like __import__ and open. It looks like this:
This feels safe. It is not. Python objects carry references to their classes, and classes carry references to their parent classes and their subclasses. From any object, you can walk back up to object and from there see every class loaded into the interpreter. That includes things you wanted to hide.
From this list, an attacker walks to a class that wraps file I/O, subprocess, or code execution, and uses it. There are public proof-of-concept payloads that, given a "restricted" eval, read arbitrary files or execute shell commands in fewer than 100 characters. The Python community has spent decades trying to plug these holes and the answer keeps coming back the same: you cannot safely run untrusted Python code in a Python interpreter that runs your own code too. Process isolation, OS sandboxes, or WebAssembly are real answers. Dictionary tricks are not.
The takeaway: passing restricted `globals` to `eval` does not make it safe for untrusted input. Treat that pattern as a code smell on its own.
ast.literal_evalA lot of the time, people reach for eval because they want to parse a string into a Python data structure. A config file might store a list. A database column might hold a dict. A test fixture might want a tuple.
For that exact case, the standard library ships ast.literal_eval. It parses the same syntax as eval for literal data structures only: numbers, strings, tuples, lists, dicts, sets, booleans, and None. It refuses anything else (function calls, attribute access, operators beyond unary +/-, names). That's exactly the safety guarantee you want.
Feed it the attack string from earlier and it refuses, cleanly.
It also accepts the same numeric, string, and container literal syntax Python source files use, so you can store nested structures and round-trip them.
For anything that looks like "parse Python literal data from a string", ast.literal_eval is the answer. For data you control end-to-end, JSON via the json module is usually even better because it's a standard interchange format that other languages can read too. Reach for ast.literal_eval when the source genuinely contains Python literals (tuples, sets, single-quoted keys) that JSON can't represent.
| Function | Accepts | Returns | Safe for untrusted input? | Typical use |
|---|---|---|---|---|
eval(expr) | A single expression as a string | The expression's value | No, even with restricted globals | REPL prompts, trusted dynamic config, formula evaluation in trusted contexts |
exec(code) | One or more statements as a string | None (state lives in the namespace dict) | No, even with restricted globals | Code generation, plugin loading from trusted sources, dynamic class building |
ast.literal_eval(s) | A string containing a Python literal (numbers, strings, tuples, lists, dicts, sets, booleans, None) | The parsed Python value | Yes | Parsing literal data from config files, database columns, logs, or user input |
If you remember nothing else from this lesson, remember the last row.
A lot of "I need eval" turns out, on inspection, to be a structural problem the language already solves. Before reaching for eval or exec, check whether one of these patterns fits.
Dispatch dictionary. When you want to pick a function by name, map names to functions in a dict.
The dispatch dict is faster (no parsing) and safer (the user can only pick from keys you defined).
`getattr` for method lookup. When the "name" maps to a method on an object, use getattr instead of building "obj.method()" and calling eval.
`operator` module for arithmetic. If you wanted to evaluate things like price + tax or qty * unit_price driven by a config, the operator module has named functions for every operator. Combine with the dispatch dict pattern.
`json.loads` for data interchange. If the strings come from a database column, a config file you control, or another service, JSON is usually the right format. It's faster than ast.literal_eval, has tooling in every language, and is unambiguously safe.
The rule of thumb: any time you're tempted to write eval(name + "(" + arg + ")"), stop. There's almost always a cleaner way using ordinary Python.
eval and exec Are Actually FineThis lesson is heavy on "don't", but there are genuine uses. The common thread: the code being evaluated is trusted (you wrote it, or it comes from a source you fully control), and the alternative is significantly more painful.
python -i are built on top of compile + exec. They evaluate code the user typed because that's the entire point. The boundary is "the user is the trust boundary".dataclasses module, which builds __init__ strings and execs them) use exec in a controlled way on strings the framework itself constructed.exec may be the right tool to load them dynamically. Plugins that come from third parties need real sandboxing (subprocess, container, WebAssembly), not exec.pdb evaluate user expressions in the context of a stopped frame. Same trust model as the REPL.If your case doesn't look like one of those, default to "use ast.literal_eval, a dispatch dict, or JSON" instead.
Tying the lesson together. A store wants to let internal admins define product-level discount rules. The naive version is unsafe. The safer version uses ast.literal_eval to parse structured data and a dispatch dict to pick the discount logic.
Now the configuration format is structured data (which ast.literal_eval parses safely), and the actual computation is plain Python that you wrote and reviewed. An attacker who tries to inject "__import__('os').system('...')" into the rule string gets a ValueError from ast.literal_eval, not code execution.
That's the shape most "I need eval" problems take when you look at them carefully: parse data with ast.literal_eval (or JSON), pick behavior with a dispatch dict, do the actual work in real Python code.
For application code, treat eval and exec as restricted tools. The default answer is "don't". If you're reaching for them, ask:
getattr, or operator give me the same behavior?ast.literal_eval do it?If after all that you still need real eval or exec, you're probably writing a REPL, a debugger, a code generator, or something similar. Those are valid uses. Most application code is not one of them.
10 quizzes