Python Bytecode Instruction Set
CPython does not execute your source text directly. The compiler lowers each function body into a flat array of bytecode instructions — the instruction set of a small stack machine — and the evaluation loop executes those one at a time against a value stack. Since Python 3.6 the encoding is wordcode: every instruction occupies exactly two bytes, an 8-bit opcode selecting the operation and an 8-bit oparg carrying its argument (InternalDocs/interpreter.md). The instruction set is large (well over a hundred opcodes in 3.14), grouped into families by what they do — stack shuffling, loads and stores, operators, control flow, calls — and, since 3.11, shadowed by a parallel set of specialized opcodes that the running interpreter swaps in for hot code. Critically, this instruction set is not a stable API: opcodes are added, removed, renumbered, and re-specified in every minor release, so any code that reads or generates bytecode is pinned to one CPython version (dis docs).
This note describes the encoding and the catalogue — the shape of an instruction, how arguments larger than a byte are smuggled in, the inline cache words that ride along with specializable opcodes, and the categories of operations. How the loop dispatches to each handler is Instruction Dispatch and Computed Gotos; how the runtime rewrites generic opcodes into specialized ones is The Specializing Adaptive Interpreter and Inline Caches and Quickening. The bytecode array lives inside a code object (co_code), and you inspect it with [[The dis Module and Bytecode Disassembly|the dis module]].
Mental Model
Think of CPython’s bytecode as the machine code of an imaginary stack-based CPU whose only registers are a value stack (operands pushed and popped) and an instruction pointer (next_instr) that walks the bytecode array. There is no general-purpose register file; almost every instruction either pushes operands onto the stack, pops operands off it, or both. LOAD_FAST 0 pushes local variable 0; BINARY_OP 0 pops two operands and pushes their sum; STORE_FAST 1 pops the top and stores it into local 1. A whole expression like a + b becomes three pushes-and-pops, not a single arithmetic instruction.
flowchart LR subgraph code["co_code: array of 16-bit code units"] direction LR U0["RESUME 0"] U1["LOAD_FAST 0\n(a)"] U2["LOAD_FAST 1\n(b)"] U3["BINARY_OP 0\n(+)"] U4["CACHE\n(inline cache word)"] U5["RETURN_VALUE"] end U0 --> U1 --> U2 --> U3 --> U4 --> U5 U1 -. push a .-> S[("value stack")] U2 -. push b .-> S U3 -. pop b, pop a,\npush a+b .-> S
Figure: the bytecode for return a + b, shown as a linear stream of two-byte code units. The insight: each unit is (opcode, oparg), the stream is walked strictly left-to-right except where a jump rewrites the pointer, and BINARY_OP is followed by a CACHE word — extra inline storage that logically belongs to BINARY_OP, not a separate instruction. The value stack is the only place operands live between instructions.
The single most important property to internalize: the bytecode is a CPython implementation detail with no compatibility guarantee. The dis documentation states it flatly — “Bytecode is an implementation detail of the CPython interpreter. No guarantees are made that bytecode will not be added, removed, or changed between versions of Python” (dis docs). The disassembly you see in 3.14 differs materially from 3.11, which differs from 3.8. This is by design: keeping the instruction set unfrozen is exactly what let the Faster CPython project rebuild the interpreter around specialization (see The Faster CPython Project).
The Wordcode Encoding
Bytecode is stored as an array of 16-bit code units, the C type _Py_CODEUNIT. Per the internal documentation, “Each code unit contains an 8-bit opcode and an 8-bit argument (oparg), both unsigned” (InternalDocs/interpreter.md). To keep .pyc files portable across machines of differing endianness, the layout within the unit is fixed regardless of the host byte order: “opcode is always the first byte and oparg is always the second byte.”
This fixed two-byte width is what the name wordcode refers to, and it has been the encoding since Python 3.6. The dis documentation records the transition tersely: “Changed in version 3.6: Use 2 bytes for each instruction. Previously the number of bytes varied by instruction” (dis docs). Before 3.6, an instruction was one byte if it took no argument and three bytes (opcode + two-byte little-endian argument) if it did — a variable-length encoding. Fixed-width wordcode is simpler to decode (every instruction advances next_instr by a constant amount before accounting for caches), simpler to jump within (jump targets are instruction offsets, not byte offsets — a 3.10 change noted in the dis docs), and aligns naturally for the inline-cache scheme below.
The decoder in the evaluation loop reads one code unit and splits it. The 3.14 macro that does this, from ceval_macros.h, is:
#define NEXTOPARG() do { \
_Py_CODEUNIT word = {.cache = FT_ATOMIC_LOAD_UINT16_RELAXED(*(uint16_t*)next_instr)}; \
opcode = word.op.code; \
oparg = word.op.arg; \
} while (0)Line by line: next_instr is a _Py_CODEUNIT* pointing at the current instruction. FT_ATOMIC_LOAD_UINT16_RELAXED reads the 16-bit unit as a single atomic load — the atomicity matters in the free-threaded build, where another thread may be specializing this very instruction concurrently, but on the default build it compiles to a plain load. word.op.code extracts the opcode byte and word.op.arg the oparg byte. After this, control passes to the handler for opcode; see Instruction Dispatch and Computed Gotos.
EXTENDED_ARG: arguments larger than one byte
One byte of oparg gives a range of 0–255. Plenty of real arguments exceed that: a function with more than 256 constants, a jump spanning more than 255 instructions, a name table with hundreds of entries. The instruction set solves this with a prefix opcode, EXTENDED_ARG, that carries the high-order bytes of an oversized argument.
The mechanism, per the internal docs: “the EXTENDED_ARG opcode allows us to prefix any instruction with one or more additional data bytes, which combine into a larger oparg” (InternalDocs/interpreter.md). The worked example given there: the sequence
EXTENDED_ARG 1
EXTENDED_ARG 0
LOAD_CONST 2
“would set opcode to LOAD_CONST and oparg to 65538 (that is, 0x1_00_02).” Read it as base-256 digits accumulating from most-significant to least: the first EXTENDED_ARG 1 contributes 0x01, the second EXTENDED_ARG 0 shifts that left and adds 0x00, and the real LOAD_CONST 2 shifts again and adds 0x02, yielding 0x010002 = 65538. The decode loop literally does oparg = (oparg << 8) | next_byte for each EXTENDED_ARG it walks before reaching the real opcode.
The compiler caps the prefix chain: “The compiler should limit itself to at most three EXTENDED_ARG prefixes, to allow the resulting oparg to fit in 32 bits” (InternalDocs/interpreter.md). Three prefix bytes plus the one in the real instruction give four bytes total — a 32-bit argument ceiling. The dis documentation phrases the same rule from the opcode’s own perspective: “For each opcode, at most three prefixal EXTENDED_ARG are allowed, forming an argument from two-byte to four-byte” (dis docs). Because EXTENDED_ARG is itself a real two-byte code unit, a single logical instruction with a huge argument can physically occupy up to eight bytes.
CACHE: inline cache words
Some instructions are followed in the bytecode array by one or more CACHE code units. These are not instructions the interpreter ever “executes” in the ordinary sense; they are scratch space reserved inside the instruction stream for the specializing interpreter to stash runtime data — a cached type version, a resolved attribute offset, an adaptive countdown. The dis documentation describes the CACHE opcode as marking “extra space for the interpreter to cache useful data directly in the bytecode itself,” and adds the load-bearing clause: “Logically, this space is part of the preceding instruction” (dis docs). The internal docs describe the same words as “one or more two-byte entries included in the bytecode array as additional words following the opcode/oparg pair,” zero-initialized by the compiler.
By default dis hides cache words; pass show_caches=True to reveal them. “Changed in version 3.11: Some instructions are accompanied by one or more inline cache entries, which take the form of CACHE instructions” (dis docs). How those bytes are read and written, and why putting the cache in the instruction stream (rather than a side table) is the cheap option, is the subject of Inline Caches and Quickening. For this note the point is structural: the number of CACHE words after an opcode is fixed per opcode, so next_instr must skip them to reach the next real instruction — the per-opcode “instruction size” is 1 + number_of_caches code units.
The Opcode Categories
The instruction set is best learned by family. The names below are 3.14 opcodes; the numeric values are deliberately omitted because they are unstable across releases (use dis.opmap to get the current mapping for your interpreter). All examples assume the default (unspecialized) opcodes; their specialized cousins appear in the final section.
Stack manipulation. The plumbing that rearranges the value stack without touching variables. POP_TOP discards the top (STACK.pop()). COPY(i) duplicates the i-th-from-top onto the top without removing it (STACK.append(STACK[-i])). SWAP(i) exchanges the top with the i-th-from-top (STACK[-i], STACK[-1] = STACK[-1], STACK[-i]). PUSH_NULL (since 3.11) pushes a NULL placeholder used to set up the call sequence (dis docs).
Loads and stores. Moving values between the stack and the various name scopes. Local variables — the fastest case, an array index rather than a dict lookup (see Fast Locals and the LOAD_FAST Family) — use LOAD_FAST(i) / STORE_FAST(i). Constants come from LOAD_CONST(i), indexing the code object’s co_consts tuple. Globals and builtins use LOAD_GLOBAL(namei) / STORE_GLOBAL. Attributes use LOAD_ATTR(namei) / STORE_ATTR for obj.name. Closure/cell variables use LOAD_DEREF / STORE_DEREF (see Cell Variables and Closures). Python 3.14 adds several fast-path loads: LOAD_SMALL_INT(i) directly pushes a small integer in range(256) without going through the constants table; LOAD_FAST_BORROW(i) pushes a borrowed reference to a local (it skips the reference-count increment because the local provably outlives the borrow), and LOAD_FAST_BORROW_LOAD_FAST_BORROW fuses two such loads into one instruction; LOAD_COMMON_CONSTANT pushes a well-known builtin like AssertionError (dis docs, What’s New in 3.14).
Operators. Arithmetic, comparison, and membership. The big one is BINARY_OP(op), which since 3.11 handles all binary arithmetic and in-place operators through a single opcode whose oparg selects the operation (+, -, *, //, +=, …): it pops the right- and left-hand operands and pushes the result. In 3.14, BINARY_OP with the NB_SUBSCR oparg also subsumes subscripting, replacing the former dedicated BINARY_SUBSCR opcode (dis docs). Comparisons use COMPARE_OP(opname) (for <, <=, ==, …), while identity and membership have their own opcodes, IS_OP(invert) (is / is not) and CONTAINS_OP(invert) (in / not in). Unary operators (-x, ~x, not x) have UNARY_NEGATIVE, UNARY_INVERT, UNARY_NOT, and the truthiness test TO_BOOL.
Control flow. Conditional and unconditional jumps, plus iteration. CPython’s jump model is relative: a jump’s oparg is a signed-in-effect instruction offset from the current position, not an absolute address. Since 3.13 all jumps are relative — the dis collections hasjrel/hasjabs were collapsed into a single hasjump, with hasjabs now empty (dis docs). Unconditional jumps are JUMP_FORWARD(delta) and JUMP_BACKWARD(delta); the backward jump additionally checks for pending interrupts (it is the back-edge of every loop, so it is the natural place to poll — see The eval Breaker). Conditional jumps pop a value and branch on it: POP_JUMP_IF_TRUE, POP_JUMP_IF_FALSE, and the dedicated POP_JUMP_IF_NONE / POP_JUMP_IF_NOT_NONE (3.11+) that avoid a separate comparison against None. Iteration is driven by FOR_ITER(delta), which peeks the iterator on top of the stack, calls its __next__, pushes the yielded value, and — on StopIteration — jumps past the loop body by delta instead of raising; 3.14 adds POP_ITER to clean the exhausted iterator off the stack (dis docs).
Calls. Invoking a callable is a small choreography. The caller pushes the callable (and, for method calls set up by LOAD_METHOD/LOAD_ATTR, a NULL or self), pushes the arguments, then issues CALL(argc) for the plain positional case. Keyword arguments go through CALL_KW(argc) (3.13+), which takes a tuple of keyword names; fully dynamic f(*args, **kwargs) uses CALL_FUNCTION_EX(flags). Building the callable object itself is MAKE_FUNCTION plus SET_FUNCTION_ATTRIBUTE(flag) (3.13+) for defaults, annotations, and closures. A representative disassembly from the docs:
def myfunc(alist):
return len(alist)
2 RESUME 0
3 LOAD_GLOBAL 1 (len + NULL)
LOAD_FAST_BORROW 0 (alist)
CALL 1
RETURN_VALUEHere LOAD_GLOBAL pushes both len and a trailing NULL (the + NULL annotation), LOAD_FAST_BORROW pushes the argument by borrowed reference, CALL 1 invokes with one positional argument, and RETURN_VALUE pops the result and returns it (dis docs). The leading RESUME is a no-op-like checkpoint emitted at function entry (and after every yield) where the interpreter can be instrumented or interrupted.
Building collections and other families. For completeness: BUILD_TUPLE/LIST/SET/MAP(count) assemble containers from count stacked items; LIST_APPEND/SET_ADD/MAP_ADD implement comprehensions; exception handling uses RAISE_VARARGS, RERAISE, CHECK_EXC_MATCH, PUSH_EXC_INFO, POP_EXCEPT (see Exception Handling Internals); generators and coroutines use YIELD_VALUE, SEND, RETURN_GENERATOR, GET_AWAITABLE (see Generators and the yield Mechanism). New in 3.14, the template string feature adds BUILD_INTERPOLATION and BUILD_TEMPLATE (What’s New in 3.14).
Numeric opcode values are intentionally unstable
Opcodes are assigned integer IDs in
Include/opcode_ids.h, which the build generates from the instruction definitions. The header is structured in bands: an “unused-argument” boundary (HAVE_ARGUMENT— opcodes below it take no oparg), a block of generic opcodes, then a band of specialized opcodes, and a high band of instrumented opcodes used bysys.monitoring. Reading the v3.14.5Include/opcode_ids.hdirectly, the boundaries in this release areHAVE_ARGUMENT = 43,MIN_SPECIALIZED_OPCODE = 129,MIN_INSTRUMENTED_OPCODE = 234, with individual generic opcodes such asRETURN_VALUE = 35,LOAD_CONST = 82, andLOAD_FAST = 84. These exact integers shift every release as opcodes are added or removed (RETURN_CONST, for instance, was id 103 in 3.13 and is gone in 3.14), so never hard-code them — readdis.opmap/dis.opnameat runtime instead.
Specialized and Adaptive Instructions
The opcodes above are what the compiler emits. The running interpreter then rewrites hot ones in place into specialized variants. This was introduced as the Specializing Adaptive Interpreter in 3.11 (PEP 659) and is the headline performance story of modern CPython; the per-instruction mechanism is documented in InternalDocs/interpreter.md.
The key idea, stated at the level of the instruction set: each “specializable” generic opcode is the head of a family of opcodes. The internal docs define a family as “an adaptive instruction along with the specialized instructions that it can be replaced by,” and require that “all members of a family have the same number of inline cache entries” so the rewrite is a same-size, in-place patch of one code unit (InternalDocs/interpreter.md). The naming convention makes families legible in a disassembly: “All instruction names should start with the name of the adaptive instruction,” with a suffix describing the specialized case. So LOAD_ATTR is the family head, and its specialized members include LOAD_ATTR_INSTANCE_VALUE (the attribute lives in the object’s inline value array), LOAD_ATTR_SLOT (a __slots__ attribute at a fixed offset), and LOAD_ATTR_MODULE. Likewise LOAD_GLOBAL specializes to LOAD_GLOBAL_MODULE and LOAD_GLOBAL_BUILTIN; BINARY_OP to BINARY_OP_ADD_INT, BINARY_OP_ADD_UNICODE, and so on; CALL to CALL_PY_EXACT_ARGS, CALL_BUILTIN_FAST; FOR_ITER to FOR_ITER_LIST, FOR_ITER_RANGE; COMPARE_OP to COMPARE_OP_INT (dis docs, specialize.c).
A specialized opcode trades generality for speed: BINARY_OP_ADD_INT assumes both operands are exact ints and does the addition inline, with a cheap guard up front that checks the assumption and falls back (“deoptimizes”) to the generic BINARY_OP if it fails. That guard reads the inline CACHE words. The mechanism — how a counter decides when to attempt specialization, how guards deopt, the cooldown after a failed attempt — is deliberately out of scope here; see The Specializing Adaptive Interpreter and Inline Caches and Quickening. For the instruction-set picture, three facts suffice: (1) specialized opcodes occupy their own band of opcode IDs; (2) they are never emitted by the compiler and never written to .pyc files — they exist only in the live, in-memory bytecode of a running process; and (3) dis shows them only with adaptive=True, since “Changed in version 3.11: The adaptive bytecode can be shown by passing adaptive=True” (dis docs).
Where the Instruction Set Comes From
A point that surprises newcomers: the instruction semantics are no longer written as hand-maintained C in the eval loop. They live in a single C-like DSL file, Python/bytecodes.c, and a code generator (Tools/cases_generator/) emits the actual switch cases (generated_cases.c.h), the tier-2 micro-op cases (executor_cases.c.h), the computed-goto jump table (opcode_targets.h), the opcode IDs (opcode_ids.h), and the dis metadata — all from that one source of truth — the internal docs describe the generic switch as “generated from the instruction definitions in Python/bytecodes.c” (InternalDocs/interpreter.md). This is why the instruction set can be reshaped each release without the encoding drifting: there is one authoritative definition, and everything else is generated from it. The compiler side that emits this bytecode is Bytecode Compilation.
Common Misunderstandings
“Bytecode is portable / stable, like Java bytecode.” No. Java froze its instruction set as a published spec; CPython explicitly did not. A .pyc compiled by 3.13 will be rejected by 3.14 (the magic number differs — see Bytecode Caching and pyc Files), and a tool that parses co_code must be rewritten for each release. The dis docs warn that the module “should not be considered to work across Python VMs or Python releases” (dis docs).
“Each instruction is one byte (hence ‘bytecode’).” Historically true (pre-3.6, the no-argument case was one byte), but since 3.6 every instruction is a fixed two-byte word, and a single logical instruction can span up to four words once EXTENDED_ARG prefixes and CACHE suffixes are counted. The name “bytecode” is a holdover.
“CACHE is a real instruction the interpreter runs.” No — it is reserved data inside the stream, logically part of the preceding instruction, and the dispatcher skips over it. Counting CACHE words as executable instructions will misalign any hand-written decoder.
“The opcodes I see are the ones that execute.” Only at first. Once code is hot, the in-memory bytecode is studded with specialized opcodes you will not see in a fresh disassembly unless you pass adaptive=True and the code has actually run enough to specialize.
See Also
- Instruction Dispatch and Computed Gotos — how the loop jumps to each opcode’s handler
- The dis Module and Bytecode Disassembly — the tool for reading this instruction set
- The Specializing Adaptive Interpreter — how generic opcodes are rewritten into specialized families
- Inline Caches and Quickening — what the
CACHEwords hold and how they are filled - Code Objects — the container that holds
co_code,co_consts, and the name tables opargs index into - Bytecode Compilation — the compiler stage that emits these instructions
- The CPython Evaluation Loop — the loop that fetches and executes them
- Fast Locals and the LOAD_FAST Family — why local-variable access is an array index
- Bytecode Caching and pyc Files — why instruction-set changes break
.pycportability - Python Internals MOC — §3 The Bytecode Interpreter