How Python Implements the if Statement at the Bytecode Level
The article dissects Python's CPython implementation of the if‑elif‑else control‑flow construct, detailing the generated bytecode, the POP_JUMP_IF_FALSE/TRUE instructions, jump macros, helper truth‑testing functions, computed‑goto support, and the role of instruction prediction in the virtual machine.
if bytecode
Running the following Python snippet with dis.dis(compile(...)) yields the bytecode shown below. The comments annotate each step.
import dis
code_string = """
score = 90
if score >= 85:
print("Good")
elif score >= 60:
print("Normal")
else:
print("Bad")
"""
dis.dis(compile(code_string, "<file>", "exec")) 0 RESUME 0
2 LOAD_CONST 0 (90)
4 STORE_NAME 0 (score)
6 LOAD_NAME 0 (score)
8 LOAD_CONST 1 (85)
10 COMPARE_OP 92 (>=)
14 POP_JUMP_IF_FALSE 9 (to 34)
16 PUSH_NULL
18 LOAD_NAME 1 (print)
20 LOAD_CONST 2 ('Good')
22 CALL 1
30 POP_TOP
32 RETURN_CONST 6 (None)
34 LOAD_NAME 0 (score)
36 LOAD_CONST 3 (60)
38 COMPARE_OP 92 (>=)
42 POP_JUMP_IF_FALSE 9 (to 62)
44 PUSH_NULL
46 LOAD_NAME 1 (print)
48 LOAD_CONST 4 ('Normal')
50 CALL 1
58 POP_TOP
60 RETURN_CONST 6 (None)
62 PUSH_NULL
64 LOAD_NAME 1 (print)
66 LOAD_CONST 5 ('Bad')
68 CALL 1
76 POP_TOP
78 RETURN_CONST 6 (None)Each >> marker denotes the start of a new branch in the if‑elif‑else chain. The interpreter evaluates the condition, then uses a conditional jump to skip to the next branch when the condition is false.
POP_JUMP_IF_FALSE
The C implementation (excerpted from Python/bytecodes.c) pops the top‑of‑stack object, checks its truth value, and jumps when the result is false.
TARGET(POP_JUMP_IF_FALSE) {
PyObject *cond = stack_pointer[-1];
if (Py_IsFalse(cond)) {
JUMPBY(oparg);
} else {
int err = PyObject_IsTrue(cond);
Py_DECREF(cond);
if (err > 0) {
JUMPBY(oparg);
} else if (err < 0) {
goto pop_1_error; // error path (rare)
}
}
STACK_SHRINK(1);
DISPATCH();
}Truth testing relies on helper functions such as Py_IsTrue, Py_IsFalse, PyObject_IsTrue, and PyObject_Not (see Objects/object.c).
POP_JUMP_IF_TRUE
The symmetric opcode is used for the not keyword. Its logic mirrors the false‑case version, jumping when the evaluated truth value is true.
TARGET(POP_JUMP_IF_TRUE) {
PyObject *cond = stack_pointer[-1];
if (Py_IsTrue(cond)) {
JUMPBY(oparg);
} else if (!Py_IsFalse(cond)) {
int err = PyObject_IsTrue(cond);
Py_DECREF(cond);
if (err > 0) {
JUMPBY(oparg);
} else if (err < 0) {
goto pop_1_error;
}
}
STACK_SHRINK(1);
DISPATCH();
}Jump macros
Relative forward jumps are performed by #define JUMPBY(x) (next_instr += (x)). Absolute jumps use
#define JUMPTO(x) (next_instr = _PyCode_CODE(frame->f_code) + (x)). JUMPTO can move both forward and backward, while JUMPBY only moves forward.
Computed goto (CPython 3.12)
Starting with CPython 3.12 the interpreter can dispatch opcodes via computed‑goto, which requires GCC’s “label‑as‑value” extension (e.g., goto *label_a). This eliminates the switch‑case lookup and makes the next‑instruction address known at runtime.
Instruction prediction (fallback)
When computed‑goto is disabled, the VM falls back to a switch dispatch. For opcode pairs that are highly correlated, a prediction hint is inserted using the macro PREDICTED(op) and the helper PREDICT(op):
#define PREDICTED(op) PRED_##op
#define PREDICT(op) \
do { \
_Py_CODEUNIT word = *next_instr; \
opcode = word.op.code; \
if (opcode == op) { \
oparg = word.op.arg; \
INSTRUCTION_START(op); \
goto PRED_##op; \
} \
} while (0)Only pairs with a very high likelihood (e.g., MATCH_SEQUENCE followed by POP_JUMP_IF_FALSE) receive such predictions, because the success probability must justify the extra code.
Examples of conditional jumps
Simple if 2 > 1 produces a POP_JUMP_IF_FALSE:
import dis
code_string = """
if 2 > 1:
print("ok")
"""
# prints a subset of bytecode
dis.dis(compile(code_string, "<file>", "exec"))
"""
2 LOAD_CONST 0 (2)
4 LOAD_CONST 1 (1)
6 COMPARE_OP 68 (>)
10 POP_JUMP_IF_FALSE 9 (to 30)
"""Using not flips the opcode to POP_JUMP_IF_TRUE:
import dis
code_string = """
if not 2 > 1:
print("ok")
"""
# prints a subset of bytecode
dis.dis(compile(code_string, "<file>", "exec"))
"""
2 LOAD_CONST 0 (2)
4 LOAD_CONST 1 (1)
6 COMPARE_OP 68 (>)
10 POP_JUMP_IF_TRUE 9 (to 30)
"""Multiple not operators illustrate how the VM collapses even numbers of not into a false‑jump and odd numbers into a true‑jump:
# even number of nots → POP_JUMP_IF_FALSE
code_string = """
if not not not not 2 > 1:
print("ok")
"""
# bytecode shows POP_JUMP_IF_FALSE
# odd number of nots → POP_JUMP_IF_TRUE
code_string = """
if not not not not not 2 > 1:
print("ok")
"""
# bytecode shows POP_JUMP_IF_TRUEJump offset calculation
The POP_JUMP_IF_FALSE instruction at offset 14 has an operand of 9, meaning “jump forward 9 instructions”. Each instruction is 2 bytes, so the target address is 14 + 9*2 = 32. The apparent mismatch with the annotated target 34 is due to the TARGET macro expanding to an additional next_instr++ before the jump.
Summary
The if‑elif‑else construct compiles to a linear sequence of bytecode that evaluates each condition, then uses POP_JUMP_IF_FALSE (or POP_JUMP_IF_TRUE for not) to skip to the next branch when the condition fails. Jump offsets are computed with JUMPBY (relative) or JUMPTO (absolute). CPython 3.12’s computed‑goto dispatch removes the need for instruction prediction; when computed‑goto is unavailable, the interpreter employs a lightweight prediction mechanism for opcode pairs with a strong correlation, allowing a direct jump to the predicted handler and avoiding a full switch lookup.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Satori Komeiji's Programming Classroom
Python and Rust developer; I write about any topics you're interested in. Follow me! (#^.^#)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
