Which expressions does ast.literal_eval accept, and which does it refuse?
answer
- A parser, not an evaluator
- Only literal displays survive the walk
- Names and calls are rejected
- One hardcoded exception for empty sets
- ValueError names the offending node
basics
~10 sast.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 sIt 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 linesimport 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
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.
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.
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.
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