Exploring AST Optimization Techniques in CPython's Compiler Pipeline

Beyond constant folding, CPython's abstract syntax tree (AST) optimizer implements several other transformations that reduce runtime work. These optimizations are applied during compilation, converting high-level constructs into simpler constant forms or more efficient bytecode sequences. Below we examine the key internal functions that drive this process.

The create_constant Utility

Many optimization routines rely on a helper that promotes an expression node to a constant. When a new constant value is computed (e.g., from folding 1+2 into 3), create_constant stores that value in the AST node and changes its type to Constant_kind. If the computed value is NULL (e.g., after a division‑by‑zero error), it clears the exception and returns success without modification.

static int
create_constant(expr_ty node, PyObject *value, PyArena *arena)
{
    if (value == NULL) {
        if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt))
            return 0;
        PyErr_Clear();
        return 1;
    }
    if (_PyArena_AddPyObject(arena, value) < 0) {
        Py_DECREF(value);
        return 0;
    }
    node->kind = Constant_kind;
    node->v.Constant.kind = NULL;
    node->v.Constant.value = value;
    return 1;
}

Unary Operator Folding

When a unary operator (like not, ~, +, -) is applied to a constant operand, the optimizer computes the result immediately. If the operand is not a constant, the function attempts to fold a not operator on a comparison into its inverse (e.g., not (x is y) becomes x is not y). The implementation uses a lookup table of unary operations.

static int
optimize_unary_operation(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    expr_ty operand = node->v.UnaryOp.operand;
    if (operand->kind != Constant_kind) {
        /* Try to invert comparison operators under 'not'. */
        if (node->v.UnaryOp.op == Not && operand->kind == Compare_kind
            && asdl_seq_LEN(operand->v.Compare.ops) == 1) {
            cmpop_ty op = asdl_seq_GET(operand->v.Compare.ops, 0);
            cmpop_ty newop = 0;
            switch (op) {
                case Is:    newop = IsNot; break;
                case IsNot: newop = Is;    break;
                case In:    newop = NotIn; break;
                case NotIn: newop = In;    break;
                default: break;  // Eq, NotEq, Lt, etc. cannot be inverted safely
            }
            if (newop) {
                asdl_seq_SET(operand->v.Compare.ops, 0, newop);
                COPY_NODE(node, operand);
                return 1;
            }
        }
        return 1;
    }
    typedef PyObject *(*unary_op_fn)(PyObject*);
    static const unary_op_fn ops[] = {
        [Invert] = PyNumber_Invert,
        [Not]    = unary_not,
        [UAdd]   = PyNumber_Positive,
        [USub]   = PyNumber_Negative,
    };
    PyObject *newval = ops[node->v.UnaryOp.op](operand->v.Constant.value);
    return create_constant(node, newval, arena);
}

Iterable Folding

List and set literals that do not contain starred expressions can be turned into tuples (or frozensets) at compile time. This allows the constant tuple/frozenset to be reused and reduces allocation at runtime. The function fold_iterable first converts a list node into a tuple node and then attempts to produce a constant tuple from its elements. For sets, it builds a frozenset from the same tuple.

static int
fold_iterable(expr_ty arg, PyArena *arena, _PyASTOptimizeState *state)
{
    PyObject *newval;
    if (arg->kind == List_kind) {
        asdl_expr_seq *elts = arg->v.List.elts;
        Py_ssize_t n = asdl_seq_LEN(elts);
        for (Py_ssize_t i = 0; i < n; i++) {
            expr_ty e = (expr_ty)asdl_seq_GET(elts, i);
            if (e->kind == Starred_kind)
                return 1;
        }
        expr_context_ty ctx = arg->v.List.ctx;
        arg->kind = Tuple_kind;
        arg->v.Tuple.elts = elts;
        arg->v.Tuple.ctx = ctx;
        newval = make_const_tuple(elts);
    }
    else if (arg->kind == Set_kind) {
        newval = make_const_tuple(arg->v.Set.elts);
        if (newval)
            Py_SETREF(newval, PyFrozenSet_New(newval));
    }
    else
        return 1;
    return create_constant(arg, newval, arena);
}

Comparison Operator Optimization

When a in or not in comparison has a list or set literal on the right side, the optimizer converts that literal into a tuple or frozenset via fold_iterable. This transformation is safe because the containment test does not depend on mutability.

static int
optimize_comparison(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    asdl_int_seq *ops = node->v.Compare.ops;
    asdl_expr_seq *comps = node->v.Compare.comparators;
    int op = asdl_seq_GET(ops, asdl_seq_LEN(ops) - 1);
    if (op == In || op == NotIn) {
        if (!fold_iterable((expr_ty)asdl_seq_GET(comps, asdl_seq_LEN(comps)-1),
                           arena, state))
            return 0;
    }
    return 1;
}

Subscript Folding

If both the subscripted object and the index are constants, the optimizer evaluates the indexing operation at compile time. For instance, "hello"[2] becomes the constant 'l'. The function uses PyObject_GetItem to perform the lookup and then stores the result as a constant.

static int
fold_subscript(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    expr_ty obj = node->v.Subscript.value;
    expr_ty idx = node->v.Subscript.slice;
    if (node->v.Subscript.ctx != Load ||
        obj->kind != Constant_kind ||
        idx->kind != Constant_kind)
        return 1;
    PyObject *newval = PyObject_GetItem(obj->v.Constant.value,
                                         idx->v.Constant.value);
    return create_constant(node, newval, arena);
}

Tuple Folding

When a tuple literal is used in a load context, the optimizer converts it into a constant tuple. This is a straightforward call to make_const_tuple.

static int
fold_tuple(expr_ty node, PyArena *arena, _PyASTOptimizeState *state)
{
    if (node->v.Tuple.ctx != Load)
        return 1;
    PyObject *newval = make_const_tuple(node->v.Tuple.elts);
    return create_constant(node, newval, arena);
}

Practical Demonstrations

The following Python snippets show how these optimizations appear in the generated bytecode. In the first example, a list inside a containment test becomes a tuple, and a set literal used as an iterable becomes a frozan tuple.

import ast, dis

code = """
5 in [5, 6, 7]
8 not in [9]
for x in {10}:
    pass
"""
print(ast.dump(ast.parse(code, mode='exec'), indent=4))
dis.dis(code)

The second example demonstrates subscript folding: a constant string index is resolved during compilation.

code = """"abcdef"[3]"""
print(ast.dump(ast.parse(code, mode='eval'), indent=4))
dis.dis(code)

What Python Does Not Optimize

Despite these transformations, CPython leaves many classic loop optimizations unimplemented. For instance, a loop that iterates over a range that is statically known to be empty still produces bytecode for the full iteration structure. The optimizer does not eliminate such loops.

for i in range(0):
    print(i)

The corresponding bytecode still includes GET_ITER, FOR_ITER, and all related instructions, wasting both space and time.

Tags: CPython AST optimization constant folding Bytecode compiler internals

Posted on Fri, 19 Jun 2026 17:23:58 +0000 by Dream$of$uccess