skip to content

In Python, what do compile()'s 'eval', 'exec' and 'single' modes each produce?

level: middleimportance: should knowfreq 30%

answer

  1. It returns code, it runs nothing
  2. Three grammars, one builtin
  3. One of them talks to the REPL's hook
  4. Source may be a tree, not text
  5. Root node type must match the mode

basics

~20 s

All three return a code object without running anything. 'eval' accepts one expression whose value comes back from eval(); 'exec' accepts a module of statements; 'single' accepts one interactive statement and prints an expression's value through sys.displayhook.

solid answer

~50 s

`compile(source, filename, mode)` turns source into a **code object** and executes nothing. The mode picks the grammar: `"eval"` accepts exactly one expression and the resulting code object is meant for `eval()`, which returns its value; `"exec"` accepts a sequence of statements, like a module body, and the code object is for `exec()`; `"single"` accepts one interactive statement and additionally arranges for an expression statement's value to be printed via `sys.displayhook` - it is the REPL's mode. The `filename` argument is what appears in tracebacks, so a marker such as `"<formula>"` is useful. `source` can be a `str`, `bytes`, **or an AST object**, and that last form is the important one: `ast.parse()` gives you a tree to inspect, and `compile()` turns the tree you approved into runnable code. The AST's root type must match the mode, or you get `TypeError`.

code

python · 6 lines
python
import sys

print(eval(compile("40 + 2", "<demo>", "eval")))
print(exec(compile("total = 40 + 2", "<demo>", "exec")))
exec(compile("40 + 2", "<demo>", "single"))
print(sys.displayhook is not None)

go deeper

for a junior

Remember that compile() produces a code object and runs nothing, and that the mode string decides which grammar the source must fit. Knowing that 'eval' means one expression and 'exec' means statements is enough at this level.

for a middle

Explain all three modes including what makes 'single' special - the value of an expression statement goes to sys.displayhook - and that source can be an AST object whose root type must match the mode.

for a senior

Show why the AST form matters: parse, walk and validate a tree, then compile only what you approved. Also know the practical wins - compile once for a hot loop, and a recognizable filename for generated code in tracebacks.

for a principal

Own the standard for generated and validated code in the codebase: where an allowlist validator lives, who re-reviews it when the language grammar grows, and whether the product needs an expression surface at all rather than fixed computed fields.

## compile() splits "turn text into code" from "run it" `compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)` is the builtin that `eval()` and `exec()` use internally when you hand them a string. Calling it directly buys you two things: the compilation happens once instead of on every call, and there is a point in time where the code exists but has not run. It returns a **code object** - the compiled bytecode plus its constants, names and metadata. Nothing in the source executes at compile time. The only failure it can raise from ordinary input is `SyntaxError` (with `ValueError` for source containing null bytes). ## The three modes **`"eval"`** requires the whole source to be one expression. The code object evaluates to a value, so you run it with `eval()` and use the result. `compile("x = 1", "<s>", "eval")` raises `SyntaxError` - assignment is not an expression. **`"exec"`** accepts a module body: any number of statements, including `def`, `class` and `import`. Running it with `exec()` returns `None`; whatever you want out of it must be bound to a name in the namespace you supply. **`"single"`** accepts exactly one interactive statement and is the mode the interactive prompt uses. Its distinguishing behaviour is that a bare expression statement's value is passed to `sys.displayhook`, which is why typing `2 + 2` at the REPL prints `4` while the same line in a script prints nothing. If you are building a REPL-like surface - a debugger console, an admin shell, a notebook-ish cell runner - `"single"` is what makes it feel like the REPL. It is not a "safer" mode: the statement it accepts can be anything. ## The AST form is why this matters for safety `source` may be a `str`, a `bytes` object, or an **AST object**. That last form is the safe door for anything that looks like accepting a formula: ```python import ast tree = ast.parse(user_text, mode="eval") # parses, runs nothing validate(tree) # your allowlist walk code = compile(tree, "<formula>", "eval") # only now is it runnable ``` `ast.parse()` is itself a thin wrapper over `compile()` with the `ast.PyCF_ONLY_AST` flag, which is why the mode strings are the same on both. Between the parse and the compile you have a tree of ordinary Python objects you can walk with `ast.walk()` or a `ast.NodeVisitor` subclass, and reject on node type. This inverts the usual dynamic-execution problem: instead of running text and hoping, you inspect a structure and decide. The root node type must agree with the mode. `ast.parse(src, mode="eval")` yields an `ast.Expression` and compiles with `"eval"`; the default `mode="exec"` yields an `ast.Module` for `"exec"`; `"single"` corresponds to an `ast.Interactive` root. Mixing them raises `TypeError`, not `SyntaxError`, because the failure is about object types rather than about syntax. ## The remaining arguments `filename` is metadata only. It is what tracebacks and any source-line lookup will report, so passing something recognizable - `"<formula>"`, `"<generated>"` - turns an otherwise anonymous traceback into a locatable one. It does not have to exist on disk. `flags` and `dont_inherit` control `__future__` behaviour and compiler flags; the flag most people meet is the one that returns an AST instead of a code object. `optimize` mirrors the interpreter's `-O` levels: `-1` follows the running interpreter, `0` keeps assertions and docstrings, `1` strips assertions, `2` also strips docstrings. ## Compiling is not running - but it is not free either A useful and often-missed point: passing untrusted text to `compile()` does not execute it. That is genuinely true and is what makes the parse-then-inspect design possible. It is not, however, a resource-free operation - deeply nested or enormous source can exhaust the parser and raise `RecursionError` or `MemoryError`, so a length cap on the input belongs before the call, not after. And of course the code object is inert only until something runs it; the decision that matters is the one you make in between. ## Where you actually see it Beyond validation, `compile()` shows up when the same source string will be executed many times (compile once, `exec()` in a loop), when you want a stable filename in tracebacks for generated code, and in tooling that transforms an AST before running it. If none of those apply, passing the string straight to `eval()` or `exec()` is equivalent and shorter.

  • Does calling compile() on untrusted source execute any of it?
    No. It parses and compiles, producing a code object or raising `SyntaxError`; nothing in the source runs. That is exactly what makes the parse-inspect-then-decide pattern possible. It is still worth capping input length first, because pathological source can exhaust the parser and raise `RecursionError` or `MemoryError` before you get a tree to inspect.
  • What does the filename argument to compile() actually affect?
    Only metadata. It is recorded on the resulting code object and is what tracebacks and source-line lookups report, so generated code with a marker like `"<formula>"` produces a traceback you can recognize instead of an anonymous one. The file need not exist, and the argument has no effect on how the code compiles or runs.
  • When is compiling once and running many times worth the extra step?
    When the same source is executed repeatedly - a rule evaluated per row, a formula per invoice line. Compilation is the expensive half, so hoisting it out of the loop and calling `eval()` on the code object avoids re-parsing every time. It also gives you one place to attach validation and a stable filename for the generated code.

saying these in an interview costs you the question

  • Thinks compile() runs the code it is given
  • Believes 'single' mode is a safer or restricted mode
  • Says compile() cannot accept anything but a string
  • Expects an ast.Module to compile in 'eval' mode
  • Assumes the filename argument must exist on disk
  • Thinks 'exec' mode code objects return the last value

context