Compiler Optimization Passes
Between the moment CPython has a complete picture of a function’s code and the moment it freezes that code into a code object, it runs a fixed sequence of compile-time optimization passes over a control-flow graph (CFG) — the program reorganized into basic blocks (straight-line runs of instructions) connected by edges (jumps). These passes fold constant expressions, delete unreachable code, collapse chains of jumps, strip redundant
NOPs, push cold blocks to the end, and fuse common opcode pairs into super-instructions. Crucially this all happens exactly once, at compile time, before any code runs — it is a different layer entirely from the runtime specializing adaptive interpreter (PEP 659) and the experimental tier-2 trace optimizer that rewrite bytecode while the program executes. As of CPython 3.14.5 the bulk of this machinery lives inPython/flowgraph.c(flowgraph.c, v3.14.5), with a thin earlier layer inPython/ast_preprocess.cand the assert/docstring handling split acrossPython/codegen.candPython/ast_preprocess.c.
This note is about the static optimizer — the work done once, at compile time, on the CFG. It is deliberately separated from its runtime counterparts:
- The specializing adaptive interpreter rewrites generic bytecodes into type-specialized ones (e.g.
BINARY_OP→BINARY_OP_ADD_INT) as a function runs, based on the types it actually observes, backed by inline caches. That is “tier 1” runtime adaptation; it never touches the constant-folding the CFG optimizer already did. - The tier-2 micro-op optimizer and the experimental JIT go further still, stitching hot specialized traces together and compiling them to machine code.
Everything below the CFG optimizer is runtime; everything in this note is compile-time. The two never overlap: the CFG optimizer has long since finished and the bytecode is frozen before the interpreter executes its first instruction.
Where in the Pipeline This Happens
CPython’s compilation pipeline turns source text into a code object through a fixed series of stages, documented at the top of compile.c (compile.c, v3.14.5):
- Check
from __future__statements. - Build the symbol table (classify every name as local, global, cell, free).
- Walk the AST in
codegen.cto emit a pseudo-instruction sequence — bytecode-like, but with some still-abstract operations and symbolic jump labels rather than offsets. - Build a CFG from that instruction sequence and run optimizations on it. ← this note
- Assemble the optimized CFG into the final bytecode and wrap it in a
PyCodeObject.
Before step 3 even begins, there is an AST-preprocessing pass (_PyAST_Preprocess in ast_preprocess.c) that does a small amount of optimization directly on the tree — not arithmetic folding (that moved out years ago; see the history section), but three specific tree rewrites covered below: __debug__ substitution, docstring removal under -OO, and printf-style %-format rewriting. So there are genuinely two layers of compile-time optimization: a thin AST-tree layer and the much larger CFG layer. Most people mean the CFG layer when they say “the optimizer,” and that is where this note spends most of its words.
flowchart TD src["Source text"] --> ast["AST<br/>(parser builds tree)"] ast --> pre["AST preprocess<br/>(ast_preprocess.c)<br/>__debug__ -> bool<br/>-OO: drop docstring<br/>percent-format rewrite"] pre --> cg["codegen.c<br/>walk AST -> pseudo-instructions<br/>(-O: assert emits nothing)"] cg --> cfg["Build CFG<br/>basic blocks + edges"] cfg --> opt["optimize_cfg + friends<br/>(flowgraph.c)<br/>fold consts / dead code /<br/>jump-thread / NOP strip /<br/>cold-block push / super-instrs"] opt --> asm["assemble.c<br/>resolve labels -> offsets<br/>build exception + location tables"] asm --> code["PyCodeObject<br/>(frozen bytecode)"] code -.->|at runtime, separate layer| spec["Specializing interpreter<br/>+ tier-2 / JIT"] style opt fill:#d4edda,stroke:#28a745 style pre fill:#fff3cd,stroke:#ffc107 style spec fill:#f8d7da,stroke:#dc3545
Diagram: the two compile-time optimization layers (yellow = AST preprocess, green = CFG optimizer) sit entirely before the code object is frozen. The runtime specializing interpreter (red) is a wholly separate stage that begins only when the frozen bytecode executes. The insight to carry away: constant folding you see in dis output is green work done once — not something the interpreter discovers at runtime.
A Short History: From Peephole to CFG
Through CPython 3.9 and earlier, the bytecode optimizer was a literal peephole optimizer: the function PyCode_Optimize in Python/peephole.c slid a small window over the already-assembled, flat byte string of bytecode and rewrote local patterns. (Verified against the v3.9.20 peephole.c, where PyCode_Optimize is defined at line 230.) Working on flat bytecode is awkward — a “peephole” cannot easily reason about whether a jump target is reachable, because control flow is implicit in jump offsets that the optimizer would have to recompute by hand after every edit.
The cutover to the CFG-based optimizer happened in CPython 3.10, not 3.11: Python/peephole.c is gone from the v3.10.x tree (it returns HTTP 404 at the v3.10.15 tag), and v3.10.15 compile.c instead defines optimize_cfg (line 7773) and optimize_basic_block (line 7341) operating on a control-flow graph; PyCode_Optimize survives there only as a do-nothing stub (line 7928) with the comment “Optimization is now done in optimize_cfg.” In the modern v3.14.5 tree this optimizer was further relocated into Python/flowgraph.c and renamed _PyCfg_OptimizeCodeUnit.
Resolved (2026-06-01)
The pre-CFG peephole optimizer was
PyCode_OptimizeinPython/peephole.c, present through 3.9 and replaced by the CFG optimizer in 3.10 (confirmed by the file’s absence at v3.10.15 and theoptimize_cfgfunctions appearing in 3.10’scompile.c). Earlier framing of “3.10 and earlier” / “3.11 cutover” was off by one release.
What is verified against primary sources is the modern shape. The 3.14 InternalDocs/compiler.md (compiler.md, v3.14.5) states plainly that step 4 builds a CFG and that _PyCfg_OptimizeCodeUnit() “applies various peephole optimizations” on that graph — the word “peephole” survived, but the optimizer now operates on a real control-flow graph of basic blocks, not a flat byte string. Operating on the graph is what makes reachability analysis (dead-code elimination) and jump threading across blocks tractable, because the edges are explicit data structures the optimizer can follow and recount.
A second, smaller migration is also load-bearing for this note: constant folding itself moved out of the AST optimizer and into the CFG optimizer. In 3.14 the AST-stage astfold_expr for a BinOp node merely recurses into its operands and then calls fold_binop — and that fold_binop (ast_preprocess.c lines 368–391) only handles one case: printf-style %-formatting of a string by a tuple ("%s" % (x,)). It does not fold 2 + 3. All numeric, tuple, and unary constant folding is done later, on the CFG (fold_const_binop, fold_const_unaryop, fold_tuple_of_constants in flowgraph.c). So the common claim “constant folding happens in the AST optimizer” is false for 3.14 — it happens in the CFG optimizer.
A third piece of archaeology worth pinning: the RETURN_CONST opcode. It was added in 3.12 (“Add the RETURN_CONST instruction” — 3.12 What’s New) to fuse a LOAD_CONST immediately followed by RETURN_VALUE into one instruction. By 3.14 it no longer exists — it is absent from dis.opmap on a live 3.14.5 interpreter, and the new LOAD_SMALL_INT opcode (“Added in version 3.14”, dis docs) plus a borrowed-reference LOAD_FAST_BORROW (“Added in version 3.14”) changed how returns are spelled. A function that returns a small literal now disassembles to LOAD_SMALL_INT n / RETURN_VALUE, not RETURN_CONST. The removal landed early in the 3.14 development cycle: RETURN_CONST is still defined in Include/opcode_ids.h at the v3.14.0a1 tag (id 103, alongside INSTRUMENTED_RETURN_CONST) but is gone by v3.14.0a2 — confirmed by diffing the header across those two tags. In the released v3.14.5 opcode_ids.h there is no RETURN_CONST; RETURN_VALUE is id 35.
The Top-Level Pass Order
The whole CFG optimization is orchestrated by _PyCfg_OptimizeCodeUnit() (flowgraph.c, line 3659). It runs in this exact order:
/* preprocessing */
translate_jump_labels_to_targets(...) /* symbolic labels -> block pointers */
mark_except_handlers(...)
label_exception_targets(...)
/* the core optimizer */
optimize_cfg(g, consts, const_cache, consts_index, firstlineno);
/* post-optimizer cleanup + lowering */
remove_unused_consts(...) /* drop co_consts entries no longer referenced */
add_checks_for_loads_of_uninitialized_variables(...)
insert_superinstructions(g); /* fuse LOAD_FAST/LOAD_FAST etc. */
push_cold_blocks_to_end(g); /* basic-block reordering */
resolve_line_numbers(g, firstlineno);Inside that, optimize_cfg() itself (flowgraph.c, lines 2552–2569) is the heart, and its order matters because passes feed each other:
RETURN_IF_ERROR(check_cfg(g)); /* structural sanity */
RETURN_IF_ERROR(inline_small_or_no_lineno_blocks(...)); /* merge trivial blocks */
RETURN_IF_ERROR(remove_unreachable(g->g_entryblock)); /* dead-code elimination #1 */
RETURN_IF_ERROR(resolve_line_numbers(g, firstlineno));
RETURN_IF_ERROR(optimize_load_const(...)); /* const-load shaping */
for (basicblock *b = ...; b != NULL; b = b->b_next) {
RETURN_IF_ERROR(optimize_basic_block(...)); /* fold / jump-thread, per block */
}
RETURN_IF_ERROR(remove_redundant_nops_and_pairs(...)); /* NOP cleanup */
RETURN_IF_ERROR(remove_unreachable(g->g_entryblock)); /* dead-code elimination #2 */
RETURN_IF_ERROR(remove_redundant_nops_and_jumps(g)); /* final jump/NOP cleanup */Two observations. First, remove_unreachable runs twice — once before the per-block work and once after — because folding and jump-threading create new dead code (a jump can be threaded past a block, orphaning it). Second, the per-block optimizer overwhelmingly works by rewriting instructions into NOP in place rather than deleting them immediately; the actual array compaction is deferred to the remove_redundant_nops passes. This NOP-then-sweep design keeps instruction indices stable while a pass is mid-flight.
Constant Folding on the CFG
Constant folding evaluates an expression whose operands are all known constants at compile time, so the runtime just loads the answer. The CFG optimizer does this in three functions, dispatched from the big switch in optimize_basic_block (flowgraph.c, lines 2311+).
Binary operations — fold_const_binop
When the optimizer sees a BINARY_OP instruction (line 2854 region of the switch), it calls fold_const_binop (flowgraph.c, line 1846). The mechanism, step by step:
get_const_loading_instrs(bb, i-1, operands_instrs, 2)(line 1858) walks backwards from the instruction before theBINARY_OP, skippingNOPs, trying to collect exactly two constant-loading instructions. If the two things being added are not both constants, it returnsfalseand folding is abandoned — nothing happens.get_const_value(...)materializes the two operand objects. ALOAD_CONSTreadsco_consts[oparg]; aLOAD_SMALL_INTreconstructs the integer withPyLong_FromLong(oparg)(line 1309).eval_const_binop(lhs, op, rhs)(line 1791) actually runs the operation — it is aswitchover the numeric-protocol op codes calling the real C functions:NB_ADD→PyNumber_Add,NB_MULTIPLY→ a guarded multiply,NB_POWER→ a guarded power, and so on. So2 + 3is computed by the samePyNumber_Addthe interpreter would use.- If evaluation raised (e.g.
1/0), the error is swallowed (PyErr_Clear(), line 1885) and folding is abandoned, unless it was aKeyboardInterrupt. The point is that aZeroDivisionErrorin1/0must surface at runtime, not abort compilation — so the optimizer leaves the division in the bytecode to fail later. - On success,
nop_out(operands_instrs, 2)turns the twoLOAD_*instructions intoNOPs, andinstr_make_load_constrewrites theBINARY_OPitself into a load of the folded constant.
Verified live on 3.14.5:
def f():
x = 2 + 3 * 4 # source
return x
LOAD_SMALL_INT 14 # <- 2 + 3*4 folded to 14 at compile time
STORE_FAST 0 (x)
LOAD_FAST_BORROW 0 (x)
RETURN_VALUEThere is no BINARY_OP left; the multiply and the add were both done by the compiler. Note also LOAD_SMALL_INT 14 rather than LOAD_CONST — because 14 fits the small-int fast path (see below).
The complexity guards — why 2 ** 1000 is not folded
Folding "a" * 1_000_000 or 2 ** 100000 at compile time would bloat the code object with a giant constant and slow compilation, defeating the purpose. So the “safe” arithmetic helpers refuse to fold results that would be too large. The constants (flowgraph.c, lines 1690–1693):
#define MAX_INT_SIZE 128 /* bits */
#define MAX_COLLECTION_SIZE 256 /* items */
#define MAX_STR_SIZE 4096 /* characters */
#define MAX_TOTAL_ITEMS 1024 /* including nested collections */const_folding_safe_multiply (line 1695) checks, for two integers, that vbits + wbits <= MAX_INT_SIZE (128 bits) before multiplying; for int * str/int * tuple it bounds the resulting length against MAX_STR_SIZE / MAX_COLLECTION_SIZE. const_folding_safe_power (line 1740) bounds vbits * wbits similarly. If a guard trips, the helper returns NULL, folding is abandoned, and the operation stays in the bytecode. Verified live:
def f(): return 2 ** 1000 # 1001 bits > 128 -> NOT folded
LOAD_SMALL_INT 2
LOAD_CONST 1 (1000)
BINARY_OP 8 (**) # the ** survives to runtime
RETURN_VALUE
def g(): return 2 ** 60 # 61 bits <= 128 -> folded
LOAD_CONST 1 (1152921504606846976)
RETURN_VALUE
def k(): return "x" * 100000 # 100000 chars > 4096 -> NOT folded
LOAD_CONST 0 ('x')
LOAD_CONST 1 (100000)
BINARY_OP 5 (*)
RETURN_VALUEThis is a genuinely useful thing to know: a constant expression in source is not guaranteed to be a constant in bytecode. 2 ** 1000 evaluates at runtime, every call.
Unary operations — fold_const_unaryop
fold_const_unaryop (flowgraph.c, line 1936) handles UNARY_NEGATIVE, UNARY_INVERT, UNARY_NOT, and the CALL_INTRINSIC_1 / INTRINSIC_UNARY_POSITIVE form of unary +. The interesting edge case is in eval_const_unaryop (line 1894): UNARY_INVERT on a bool is deliberately not folded (if (PyBool_Check(operand)) return NULL;, line 1910) because ~True is in the middle of a deprecation and the compiler must not bake in a value that may change behavior. Everything else folds, including the combination with tuple folding:
def g(): return -1, not True, ~5
LOAD_CONST 1 ((-1, False, -6)) # all three unary ops AND the tuple folded
RETURN_VALUEConstant-tuple folding — fold_tuple_of_constants
This is the pass cross-linked from CPython Tuple Internals, which documents that (1, 2, 3) is stored as a single pre-built constant. The mechanism (flowgraph.c, line 1454): when optimize_basic_block sees a BUILD_TUPLE n instruction, it calls fold_tuple_of_constants, which uses get_const_loading_instrs(bb, i-1, ..., n) to check that the n instructions feeding the BUILD_TUPLE are all constant loads. If so, it builds a real PyTuple (PyTuple_New + PyTuple_SET_ITEM in the loop, lines 1475–1489), nop_outs the element loads, and rewrites the BUILD_TUPLE into instr_make_load_const of the finished tuple. Verified live:
def g(): return (1, 2, 3)
LOAD_CONST 1 ((1, 2, 3)) # one constant, not three loads + BUILD_TUPLE
RETURN_VALUEThere is a sibling pass, fold_constant_intrinsic_list_to_tuple (line 1509), that recognizes the BUILD_LIST 0 / repeated LOAD_CONST + LIST_APPEND / CALL_INTRINSIC_1 LIST_TO_TUPLE pattern (which is how (*a_const_list,)-style tuple construction lowers) and collapses it to one LOAD_CONST of the finished tuple. Note that list and set displays are not turned into constants the way tuples are — a [1, 2, 3] must build a fresh mutable list every time, so the most the optimizer does (in optimize_lists_and_sets, line 1598) is convert the elements into a single constant tuple that a BUILD_LIST/BUILD_SET then unpacks, not the whole list.
LOAD_SMALL_INT and the const-cache
maybe_instr_make_load_smallint (flowgraph.c, line 1408) is the small-int optimization: if a folded constant is an exact int in [0, 255], the load is emitted as LOAD_SMALL_INT val rather than LOAD_CONST, avoiding a co_consts table slot entirely. This dovetails with Small Integer and String Caching — those small ints are immortal singletons (assert(_Py_IsImmortal(newconst)), line 1418). Separately, add_const/instr_make_load_const run every new folded constant through _PyCompile_ConstCacheMergeOne and a consts_index hash table (lines 1322–1357), so that identical folded constants across the function share one co_consts entry rather than duplicating.
Dead-Code Elimination
remove_unreachable (flowgraph.c, line 996) is a straightforward graph reachability sweep. It zeroes every block’s predecessor count, then does a depth-first walk from the entry block following fall-through edges and jump/block_push targets, marking each reachable block b_visited and incrementing its predecessor count. Any block left with b_predecessors == 0 after the walk is unreachable, and its instructions are deleted by setting b_iused = 0 (lines 1034–1039). Because folding can turn a conditional into a constant-true/constant-false branch (and jump-threading can orphan a block), this runs both before and after the per-block optimizer. The if a: return 1 / return 2 example shows the two reachable arms surviving; an if False: arm would be folded away and then swept here.
Jump Threading and Jump Simplification
jump_thread (flowgraph.c, line 1264) collapses a jump-to-a-jump. If instruction inst jumps to a block whose first instruction target is itself a jump, then inst can be redirected to target’s ultimate destination, skipping the intermediate hop. The implementation turns inst into a NOP and appends a fresh jump straight to target->i_target (lines 1275–1279), with a guard (line 1271) against the degenerate inst->i_target == target->i_target case that would otherwise loop forever (bpo-45773). The dispatch in optimize_basic_block (lines 2358–2417) threads POP_JUMP_IF_FALSE/POP_JUMP_IF_TRUE/POP_JUMP_IF_NONE/POP_JUMP_IF_NOT_NONE/JUMP/JUMP_NO_INTERRUPT through trailing JUMPs, and additionally simplifies the pseudo-ops JUMP_IF_FALSE/JUMP_IF_TRUE: a JUMP_IF_FALSE whose target is a JUMP_IF_TRUE can skip to that block’s b_next (lines 2383–2389), because the two conditions are mutually exclusive. There is also remove_redundant_jumps (line 1159), which deletes a JUMP whose target is simply the next block (the fall-through makes the jump pointless).
Redundant-NOP Removal
Because nearly every pass creates NOPs (folding nops-out operand loads; jump-threading nops-out the original jump), a dedicated sweep removes the ones that are safe to remove. basicblock_remove_redundant_nops (flowgraph.c, line 1043) compacts a block’s instruction array, dropping a NOP only when removing it would not lose source-location information for tracing/pdb: a NOP is kept if it carries a line number that differs from both its neighbors (lines 1050–1089), because dropping it would make a line “disappear” from the line table. NOPs with no location, or whose line matches an adjacent instruction, are dropped. This is why dis output occasionally still shows a NOP — it is anchoring a line number, not wasting a cycle the optimizer missed. (And NOPs cost essentially nothing: the specializing interpreter quickens them away.) The remove_redundant_nops_and_pairs variant additionally cancels instruction pairs that undo each other.
Block Reordering and Super-Instructions
Two post-optimize_cfg passes (run from _PyCfg_OptimizeCodeUnit, not from optimize_cfg itself) finish the job:
push_cold_blocks_to_end(flowgraph.c, line 3404, invoked at line 3703) is the basic-block reordering pass. A “cold” block is one the heuristic deems not performance-critical — exception handlers and the unlikely arms of branches (b_cold/b_warmbits, line 71). Moving them to the end of the bytecode keeps the hot path contiguous, which improves the host CPU’s instruction-cache locality and branch prediction. This is the only reordering the compiler does; it is not part of the coreoptimize_cfgpeephole loop.insert_superinstructions(flowgraph.c, line 2587, invoked at line 3701) fuses adjacent common opcode pairs into single “super-instructions”:LOAD_FAST+LOAD_FAST→LOAD_FAST_LOAD_FAST,STORE_FAST+LOAD_FAST→STORE_FAST_LOAD_FAST,STORE_FAST+STORE_FAST→STORE_FAST_STORE_FAST(lines 2595–2611). The pair is encoded with both operands packed into one oparg ((arg1 << 4) | arg2), so each operand must be< 16(line 2580). This halves dispatch overhead for the most common local-variable access pattern.
Docstrings, Asserts, and the -O / -OO Flags
The optimization level is a separate, orthogonal control set by the -O/-OO command-line flags or the PYTHONOPTIMIZE environment variable, read into c->c_optimize from _Py_GetConfig()->optimization_level (compile.c, line 136). Per the command-line docs, -O sets level 1, -OO sets level 2, and PYTHONOPTIMIZE=n sets level n (so PYTHONOPTIMIZE=2 equals -OO). The level drives three behaviors, all handled outside the CFG optimizer:
__debug__ becomes a constant. In astfold_expr (ast_preprocess.c, line 622), a Load of the name __debug__ is replaced by make_const(node, PyBool_FromLong(!state->optimize)) — True at level 0, False at level ≥ 1. This is the one genuine folding the AST stage still does for names. Once __debug__ is a literal False, the CFG dead-code pass deletes the now-unreachable if __debug__: body. Verified live:
def f():
if __debug__:
return "debug"
return "prod"
# no -O -> __debug__ is True; the "prod" branch is dead and removed:
NOP
LOAD_CONST 1 ('debug')
RETURN_VALUE
# python -O -> __debug__ is False; the "debug" branch is dead and removed:
NOP
LOAD_CONST 1 ('prod')
RETURN_VALUEassert is dropped under -O. Assertions are not removed by deleting bytecode after the fact; they are simply never emitted. codegen_assert (codegen.c, line 2931) checks if (OPTIMIZATION_LEVEL(c)) return SUCCESS; (line 2945) — at any level ≥ 1 it emits nothing at all (after still issuing the “assertion is always true” warning for a parenthesized-tuple test). At level 0 it emits the test, a LOAD_COMMON_CONSTANT AssertionError, the optional message, and RAISE_VARARGS. This is why production code under -O must never rely on assert for validation that must always run.
Docstrings are stripped under -OO. astfold_body (ast_preprocess.c, line 465) checks if (docstring && (state->optimize >= 2)) and calls remove_docstring (line 440), which replaces the leading string-expression statement with a pass (or removes it). After this, the function’s __doc__ is None. Verified live: the same function shows DOC: 'I am a docstring' under -O but DOC: None under -OO. -OO is therefore the only level that affects behavior you can observe through introspection — it shrinks .pyc files but breaks any code (e.g. doctest, some help systems) that reads __doc__.
Common Misunderstandings
- “The optimizer constant-folds, so
1/0in code is caught at compile time.” No.eval_const_binopdeliberately swallows the exception and abandons folding (flowgraph.c, line 1885), leaving the division to raise at runtime. Folding never changes when an error appears. - “
(1, 2, 3)and[1, 2, 3]are both made into constants.” Only the tuple. The list must be freshly built each time because it is mutable; the optimizer can only fold the elements into a constant tuple that aBUILD_LISTunpacks. See CPython List Internals vs CPython Tuple Internals. - “Constant folding is the specializing interpreter at work.” No — folding is a compile-time CFG pass that runs once and is already baked into the bytecode you see in
dis. The specializing interpreter is a separate runtime mechanism that rewrites opcodes based on observed types; it does not fold constants. - “Big constant expressions like
2 ** 1000are precomputed.” Only if they fit the complexity guards (128 bits for ints, 4096 chars for strings, 256 items for collections). Larger ones survive as runtime operations. - “
-Omakes my code meaningfully faster.” It almost never does — its only real effects are droppingasserts and__debug__-guarded code (and-OOstrips docstrings). The big speedups come from the Faster CPython runtime work, not from-O.
Alternatives and Comparison
- PyPy does almost none of this at its bytecode level; its wins come from a tracing JIT that optimizes hot loops at runtime, including aggressive constant propagation and allocation removal that CPython’s static CFG optimizer cannot attempt. See CPython vs PyPy and Alternative Implementations.
- Statically compiled languages (cf. Go) run a far heavier optimizer (SSA-based, with inlining, escape analysis, and register allocation) ahead of time because they pay for it once and ship the result. CPython’s CFG optimizer is deliberately cheap — it must run every time a
.pyfile is compiled (unless the result is cached; see Bytecode Caching and pyc Files), so it sticks to local, fast rewrites and leaves the expensive work to the runtime tiers.
Production Notes
The practical consequence developers actually hit: never use assert for runtime validation (input checking, invariants that must hold in production), because python -O removes them silently and the official docs warn against exactly this. Use explicit if ... raise instead. Similarly, do not ship -OO if anything reads __doc__ — doctest-based test suites and some documentation tooling break because docstrings are gone. When debugging a “why is this opcode still here” question, remember the NOP-for-line-numbers rule: a stray NOP in dis output is usually anchoring a source line for the debugger, not a missed optimization. And when reasoning about constant expressions in hot code, check the complexity guards — a 256 * SOME_CONST_TUPLE may or may not be precomputed depending on the resulting size.
See Also
- CPython Compilation Pipeline — the full source → code-object journey; this note is step 4
- Bytecode Compilation — emitting the pseudo-instruction sequence that this optimizer consumes
- Code Objects — the
PyCodeObjectthe optimized bytecode is frozen into (co_consts,co_code) - Python Bytecode Instruction Set — the opcodes (
LOAD_SMALL_INT,BINARY_OP,BUILD_TUPLE,NOP) this note manipulates - The dis Module and Bytecode Disassembly — how to see the optimizer’s output
- CPython Tuple Internals — documents the
(1,2,3)constant-tuple folding from the data-structure side - The Specializing Adaptive Interpreter — the runtime tier-1 optimizer this note is explicitly distinct from
- Trace-Based Optimization in CPython — the runtime tier-2 optimizer, also distinct from this compile-time work
- Small Integer and String Caching — why folded small ints become
LOAD_SMALL_INTof immortal singletons - Python Internals MOC — §2 The Compilation Pipeline