Skip to main content

Python/ast_unparse.c

ast_unparse.c walks a Python AST and emits the corresponding source text as a str object. It is the inverse of the parser: given a mod_ty, stmt_ty, or expr_ty node, it produces a canonical string representation. The most visible user-facing call-site is the f-string = specifier (f"{x=}"), which calls _PyAST_Unparse to embed the verbatim expression text next to its runtime value.

The file is self-contained: it carries its own growable string buffer, operator precedence table, and recursive visitor. No Python objects are allocated until the final step when the buffer is converted to a PyObject * string.

Map

LinesSymbolRole
1-40includes, _Unparser structBuffer state: char *buffer, Py_ssize_t len, Py_ssize_t alloc, precedence stack
41-80_PyUnicodeWriter-based helpersappend_charp, append_str, append_char write into the growable buffer
81-140maybe_expand_bufferDoubles allocation when capacity is exhausted
141-200unparse_expr dispatchSwitches on expr_ty->kind; calls the per-node helpers below
201-280unparse_Name, unparse_ConstantLeaf nodes; Constant handles int, float, complex, bool, None, bytes, str
281-370unparse_BinOp, unparse_UnaryOpInfix/prefix operators; inserts parentheses based on precedence comparison
371-450unparse_BoolOpand / or chains; left-associativity handled explicitly
451-530unparse_CompareMultiple comparators (a < b < c)
531-620unparse_CallPositional args, keyword args, *args, **kwargs
621-700unparse_Attribute, unparse_SubscriptDotted names and index/slice notation
701-790unparse_Slice, unparse_Indexa:b:c slices; bare Index wrapper (removed in 3.9 but kept for compat)
791-870unparse_JoinedStrF-string body; recursively unparses nested format specs
871-940unparse_IfExp, unparse_LambdaTernary and lambda; lambda argument list reuses unparse_arguments
941-1020unparse_arguments, unparse_argFunction argument lists including defaults, /, *, **
1021-1100unparse_comprehension, unparse_ListComp, unparse_SetComp, unparse_DictComp, unparse_GeneratorExpComprehension forms
1101-1160unparse_Starred, unparse_Tuple, unparse_List, unparse_Set, unparse_DictCollection literals
1161-1200_PyAST_Unparse, _PyAST_ExprAsUnicodePublic entry points; convert buffer to PyObject *

Reading: entry points

_PyAST_Unparse is the sole public entry point for general use. It allocates an _Unparser on the stack, dispatches to unparse_expr or unparse_stmt, and finalises the buffer into a PyObject *:

// Python/ast_unparse.c:1161
PyObject *
_PyAST_Unparse(expr_ty ast, PyArena *arena)
{
_Unparser u;
_unparser_init(&u);
if (unparse_expr(&u, ast, PR_TEST) < 0) {
_unparser_dealloc(&u);
return NULL;
}
return _unparser_finish(&u);
}

_PyAST_ExprAsUnicode is a thin wrapper used by the compile module when it needs the unparsed form of a default argument expression for error messages.

Reading: precedence and parenthesisation

The precedence table drives automatic parenthesisation. Each operator level is an integer constant (PR_*). unparse_BinOp compares the current expression's precedence against the enclosing context level and wraps with parentheses only when necessary:

// Python/ast_unparse.c:285
static int
unparse_BinOp(_Unparser *u, expr_ty e, int level)
{
int pr = get_op_precedence(e->v.BinOp.op);
int left_pr = pr;
int right_pr = pr + 1; /* left-associative: right side needs higher */

if (level > pr && append_charp(u, "(") < 0) return -1;
if (unparse_expr(u, e->v.BinOp.left, left_pr) < 0) return -1;
if (append_charp(u, operator_string(e->v.BinOp.op)) < 0) return -1;
if (unparse_expr(u, e->v.BinOp.right, right_pr) < 0) return -1;
if (level > pr && append_charp(u, ")") < 0) return -1;
return 0;
}

Reading: f-string unparsing

F-string nodes (JoinedStr) contain a mix of Constant (literal segments) and FormattedValue (interpolated expressions). unparse_JoinedStr reconstructs the f"..." literal, re-escaping braces and recursing into format specs:

// Python/ast_unparse.c:793
static int
unparse_JoinedStr(_Unparser *u, expr_ty e, int level)
{
if (append_charp(u, "f\"") < 0) return -1;
asdl_seq *values = e->v.JoinedStr.values;
for (Py_ssize_t i = 0; i < asdl_seq_LEN(values); i++) {
expr_ty v = asdl_seq_GET(values, i);
if (v->kind == Constant_kind) {
/* literal segment - re-escape { and } */
if (append_fstring_constant(u, v->v.Constant.value) < 0) return -1;
} else {
/* FormattedValue: {expr!conv:spec} */
if (unparse_FormattedValue(u, v, PR_TEST) < 0) return -1;
}
}
return append_charp(u, "\"");
}

The = debug specifier is handled one level up in ceval.c: it calls _PyAST_Unparse on the FormattedValue's expression and prepends expr= to the formatted output.

Port status

Not yet ported to gopy. The gopy compiler currently relies on the parser's source-location tracking rather than round-tripping through an unparser. Porting will become relevant when gopy needs to reproduce CPython's f-string = debug output exactly or when the ast module's unparse() function is implemented.