skip to content

Which expressions does ast.literal_eval accept, and which does it refuse?

level: middleimportance: must knowfreq 60%

answer

  1. A parser, not an evaluator
  2. Only literal displays survive the walk
  3. Names and calls are rejected
  4. One hardcoded exception for empty sets
  5. ValueError names the offending node

basics

~10 s

ast.literal_eval evaluates only literal structures: strings, bytes, numbers, booleans, None, Ellipsis, and the tuple, list, dict and set displays built from them. Any name, attribute, operator or function call raises ValueError instead of running.

solid answer

~40 s

It parses the source with `ast.parse` in eval mode and then walks the tree, rebuilding values from a fixed whitelist of node types: constants, tuple/list/dict/set displays, a unary sign on a number, a `+`/`-` whose right operand is complex (so `1 + 2j` works but `1 + 2` does not), and one hardcoded special case, the empty-set call `set()`, added in 3.9. Nothing is compiled or executed. Anything else raises `ValueError` with the offending node quoted — any `Name`, so `true`, `null` and `nan` all fail; any other call; attribute access; f-strings; `[0] * 3`. Unparsable input raises `SyntaxError`, which is not a `ValueError`, and an unhashable dict key raises `TypeError`, so a production call site catches all three.

code

python · 9 lines
python
import ast

print(ast.literal_eval("{'ids': [1, 2], 'ok': True, 'note': None}"))
print(ast.literal_eval("set()"), ast.literal_eval("-3"), ast.literal_eval("1 + 2j"))
for src in ("true", "1 + 2", "open('/etc/passwd')", "[0] * 3"):
    try:
        ast.literal_eval(src)
    except ValueError as exc:
        print(src, "->", type(exc).__name__)

go deeper

for a junior

Recall the one-line rule: it turns a string into a value only when that string is a plain literal — numbers, strings, lists, dicts, True/False/None — and raises an error for anything that would run code.

for a middle

Be ready to name the whitelist and the refusals precisely, including that true and null fail because they are names, and to explain that the function parses to a tree and walks it rather than compiling anything.

for a senior

Show that you catch SyntaxError and TypeError alongside ValueError at the call site, and state the guarantee narrowly: it prevents code execution, not an expensive parse of a large input.

for a principal

Own the boundary rule for the codebase: literal parsing for Python-produced strings only, a real format parser everywhere else, and a review habit that treats any eval on request data as a defect.

### A parser with a whitelist, not an evaluator `ast.literal_eval` takes a string of Python source (or an already-parsed AST node) and returns a Python value. Internally it calls `ast.parse(source, mode="eval")` to build an abstract syntax tree, then walks that tree and rebuilds values from a **fixed set of node types**. Nothing is compiled to bytecode and no code object ever runs. That is the entire point of the function: the input stays data from beginning to end, so there is no call an attacker can reach through it. ### What it accepts * **Constants** — `str`, `bytes`, `int`, `float`, `complex` literals, `True`, `False`, `None` and `Ellipsis` (`...`). * **Container displays** built from accepted elements — tuples, lists, dicts and set displays: `{'ids': [1, 2], 'ok': True}` is fine. * **A signed number** — a unary `+` or `-` applied to a numeric constant, so `-3` parses. * **A `+`/`-` binary operation whose right operand is complex**, so `1 + 2j` parses. Python has no complex *literal* with a real part; `1 + 2j` is genuinely a `BinOp` in the grammar, and the converter carves out exactly that shape so complex numbers round-trip. * **One hardcoded call: `set()`** with no arguments, added in 3.9, because the empty set has no display syntax. It is a text-level special case in the converter, not a general "safe builtins" list. Implicit string concatenation (`'a' 'b'`) also works, but only because the *parser* folds it into a single constant before the walk ever begins. ### What it refuses Everything else, including things that look harmless: * any **`Name`** — which is why `true`, `null`, `nan` and `inf` all fail; those are JSON and C spellings, not Python literals; * any **call other than the empty `set()`** — `int('7')`, `len([])`, `open('/etc/passwd')`, `Decimal('1.5')`; * any **attribute access** — `os.path`, `datetime.date`; * **f-strings**, which parse to a `JoinedStr` node containing runtime expressions; * **general arithmetic and repetition** — `1 + 2`, `[0] * 3`, `2 ** 10`; the function will not do math for you; * comparisons, comprehensions, lambdas, walrus assignments, starred unpacking. ### The exceptions it raises Catching `ValueError` alone is the most common bug in code that uses this function. Three distinct types come out of it: * **`ValueError`** — the source parsed, but the tree contains a node outside the whitelist. The message quotes the offending node: `malformed node or string on line 1: Name(id='null', ...)`. * **`SyntaxError`** — the source did not parse at all (`{'a':`, a stray `<object at 0x...>` repr). `SyntaxError` is **not** a subclass of `ValueError`, so it escapes a `except ValueError` handler. * **`TypeError`** — the tree was legal but the value cannot be built, most often an unhashable dict key: `ast.literal_eval("{[1]: 2}")` raises `TypeError: cannot use 'list' as a dict key`. On pathological input two more appear — `MemoryError` and `RecursionError` — because the *parser*, not the whitelist, hits its limits. A production call site catches the whole family. ### The check is structural, and it fails closed An important property of the whitelist is that it is written over **node types**, not over text. There is no list of forbidden function names to keep up to date and no pattern to evade with clever spelling: if the parsed tree contains any node the converter does not explicitly handle, conversion stops. That is why an unknown name, a novel syntax feature and an obfuscated call all fail the same way. When you are debugging a rejection, `ast.dump(ast.parse(src, mode="eval"))` shows you the tree the converter saw, and the node named in the `ValueError` message is exactly the one that stopped it. ### Version notes The accepted set has been stable for years, with two changes worth naming: **3.9** added the `set()` special case, and **3.10** made the function strip leading whitespace and tabs from string input, so an indented line no longer raises `SyntaxError` ("unexpected indent"). Everything above is true on **3.14**. ### Where it belongs Use it when a string that is *Python source produced by Python* must become a value again: a field in a database column holding a `repr`, a literal typed into a config UI, a doctest-style fixture, a command-line argument that should accept `[1, 2]` as well as `7`. Do **not** use it as a general data parser for another system's format — every foreign format has a real parser that is faster, stricter and gives error positions. And be precise about the guarantee: `ast.literal_eval` prevents **code execution**, not **resource exhaustion**. A hostile string can still cost enormous time and memory in the parser before the whitelist ever gets a look at the tree, so untrusted input needs a length cap in front of the call. The security claim is narrow, and stating it narrowly is what an interviewer is listening for.

  • Why does ast.literal_eval accept `1 + 2j` but reject `1 + 2`?
    Python has no complex literal with a real part — `1 + 2j` is a genuine addition node in the grammar. The converter carves out exactly that shape: a `+` or `-` whose left side is a signed int or float and whose right side is complex, so complex numbers can round-trip. `1 + 2` is ordinary arithmetic on two ints, and the function deliberately declines to do arithmetic for you.
  • Which exceptions should a call site around ast.literal_eval catch?
    `ValueError` for a node outside the whitelist, `SyntaxError` for input that does not parse, and `TypeError` for a legal tree it cannot build, such as an unhashable dict key. On hostile input add `MemoryError` and `RecursionError` from the parser itself. `SyntaxError` is not a subclass of `ValueError`, so catching `ValueError` alone is the usual bug.
  • Can you pass ast.literal_eval something other than a string?
    Yes — it also accepts an AST node, typically the result of `ast.parse(source, mode="eval")`. That is useful when you want to own the parsing step, inspect or size-check the tree first, and only then convert it. Passing a `bytes` object, by contrast, raises `ValueError`; the input must be `str` or a node.

saying these in an interview costs you the question

  • Calls it eval with a safety flag turned on
  • Claims it can call safe builtins like int or len
  • Expects true and null to parse as True and None
  • Assumes every failure raises ValueError
  • Says it executes the code in a restricted namespace
  • Thinks f-strings count as string literals here

context