AlgoMaster Logo

eval & exec (Use with Caution)

Low Priority17 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

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.

What eval Does

eval 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.

What exec Does

exec 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.

Caching with compile

When 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.

ModeWhat it acceptsUsed with
"eval"A single expressioneval
"exec"One or more statementsexec
"single"A single interactive statementexec, 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.

Why Untrusted Input Is Dangerous

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 Sandboxing Trap

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.

The Right Tool: ast.literal_eval

A 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.

Comparison Table

FunctionAcceptsReturnsSafe for untrusted input?Typical use
eval(expr)A single expression as a stringThe expression's valueNo, even with restricted globalsREPL prompts, trusted dynamic config, formula evaluation in trusted contexts
exec(code)One or more statements as a stringNone (state lives in the namespace dict)No, even with restricted globalsCode 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 valueYesParsing literal data from config files, database columns, logs, or user input

If you remember nothing else from this lesson, remember the last row.

Cleaner Alternatives

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.

When eval and exec Are Actually Fine

This 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.

  • REPLs and notebooks. Tools like IPython, Jupyter, and 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".
  • Templating and code generation. Frameworks that generate Python code from a schema (like the dataclasses module, which builds __init__ strings and execs them) use exec in a controlled way on strings the framework itself constructed.
  • Dynamic config in trusted contexts. A build system that lets developers write small Python expressions in a config file, run on the developer's own machine, is a reasonable use. The trust boundary is the developer's filesystem.
  • Plugin systems with trusted source. If plugins are first-party code shipped with the application, exec may be the right tool to load them dynamically. Plugins that come from third parties need real sandboxing (subprocess, container, WebAssembly), not exec.
  • Debuggers and profilers. Tools like 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.

A Safer Discount-Rule Example

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.

The Honest Opinion

For application code, treat eval and exec as restricted tools. The default answer is "don't". If you're reaching for them, ask:

  1. Is the input fully under my control, or does it cross a trust boundary?
  2. Could I represent this as data (JSON, a dict, an enum) instead of code?
  3. Could a dispatch dict, getattr, or operator give me the same behavior?
  4. If I still need to parse Python-ish data, can 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.

Quiz

eval & exec Quiz

10 quizzes