Python Constant Folding in AST Optimization

Python optimizes code execution speed and bytecode size by evaluating static expressions at compile time, a technique known as constant folding. In modern CPython, this optimization occurs directly on the Abstract Syntax Tree (AST) before bytecode generation. By recognizing operations involving only immutable literal values—such as arithmetic expressions, string operations, and sequence literals—the compiler computes the final value during compilation, replacing complex AST subtrees with a single constant node and reducing runtime computational overhead.

The Shift from Peephole to AST Optimization

Historically, CPython performed constant folding late in the compilation process using a bytecode peephole optimizer. This approach required generating bytecode instructions, analyzing instruction sequences, and replacing sequences of LOAD_CONST and binary operators with a single LOAD_CONST.

Starting in Python 3.7 and finalized in Python 3.8, constant folding was migrated upstream into the AST optimization phase, implemented in Python/ast_opt.c. Performing folding on the AST simplifies compiler logic, preserves semantic meaning better, and eliminates the overhead of emitting and subsequently rewriting bytecode instructions.

How the AST Optimizer Operates

The AST optimization pass runs recursively over the syntax tree before the compiler creates the control flow graph or emits bytecode. When traversing nodes, the optimizer checks whether an expression consists entirely of constants.

  1. Node Traversal: The compiler traverses the AST using a visitor pattern, descending into expressions recursively to resolve nested sub-expressions first.
  2. Literal Detection: The compiler inspects operand nodes. If the operands are instances of Constant nodes, they are candidates for folding.
  3. Evaluation: The compiler invokes the corresponding CPython C-API functions to evaluate the operation. For example, a BinOp node with an Add operator between two integer constants will invoke the equivalent of PyNumber_Add.
  4. Node Replacement: If the operation succeeds without error, the parent node (e.g., BinOp) is replaced directly in the tree with a new Constant node holding the resulting value.

Expressions Handled by Constant Folding

The AST optimizer folds several categories of static expressions:

Arithmetic and Logical Operations

Expressions involving basic numeric literals are computed immediately:

# Source
seconds = 60 * 60 * 24

# Bytecode equivalent after folding
seconds = 86400

Unary operations such as -5, +10, or not True are folded into their reduced literal values (-5, 10, False).

String and Bytes Concatenation

Static string and byte concatenations are joined into single literals:

# Source
greeting = "Hello, " + "World!"

# Bytecode equivalent after folding
greeting = "Hello, World!"

Container Literals and Membership Tests

Tuples containing only constants are constructed at compile time. In addition, lookup patterns like x in [1, 2, 3] or x in {1, 2, 3} are optimized. The compiler converts the list or set into an immutable tuple or frozenset constant:

# Source
if x in [1, 2, 3]:
    pass

# Bytecode equivalent after folding
if x in (1, 2, 3):  # Evaluated using a pre-built constant tuple
    pass

Safeguards and Constraints

Constant folding is constrained by strict rules to ensure that compilation remains fast, safe, and behaviorally identical to runtime execution:

Verifying Constant Folding

The effect of AST constant folding can be verified using the standard library's dis module:

import dis

def calculate():
    return 1000 * 60

dis.dis(calculate)

The resulting disassembly shows a single LOAD_CONST instruction rather than arithmetic operations:

2           0 LOAD_CONST               1 (60000)
            2 RETURN_VALUE

Because the AST optimizer folds the multiplication into 60000 before code generation, the final code object avoids the BINARY_OP instruction entirely.