Python dis Module: Disassembling Bytecode Explained
This article explores how Python's built-in dis module
translates raw, binary bytecode into human-readable instructions. It
covers Python’s compilation pipeline, the internal structure of code
objects, the mapping of numeric opcodes to human-readable names, and how
argument references are resolved into variables, constants, and line
numbers.
From Source Code to Bytecode
Before Python executes code, it compiles the source text into an
intermediate representation known as bytecode. The source code is first
parsed into an Abstract Syntax Tree (AST), which the compiler evaluates
to produce a code object.
A code object contains the raw execution instructions alongside metadata required by the Python Virtual Machine (PVM). Key attributes stored in a code object include:
co_code: The raw sequence of bytecode instructions as abytesobject.co_consts: A tuple containing literals and constants referenced in the scope.co_varnames: A tuple of local variable names.co_names: A tuple of global, imported, or attribute names.co_linetable: An encoding that maps bytecode offsets to source code line numbers.
The Structure of Bytecode Instructions
Starting with Python 3.6, Python uses a 16-bit "wordcode" format for its instruction set. Each instruction consists of two bytes:
- Opcode (1 byte): A number between 0 and 255 representing the specific operation (e.g., load a value, perform addition, return).
- Argument / Oparg (1 byte): A numeric index or value
between 0 and 255 used by the operation. If an instruction requires an
argument larger than 255, Python prepends an
EXTENDED_ARGopcode to expand the value.
For instructions that do not require an argument, the argument byte is set to zero and ignored by the virtual machine.
How the
dis Module Reads and Translates Bytes
The dis module acts as a decoder for the
bytes stored in co_code. When you pass a
function, method, or code object to dis.dis(), the module
performs several sequential steps to format the data into readable
text.
1. Decoding Opcodes
The dis module iterates through co_code two
bytes at a time. It takes the first byte (the numeric opcode) and maps
it to a string name using the internal lookup table
opcode.opname.
For example, if the first byte is the integer 100, the
module looks up index 100 in opname, which returns the
string 'LOAD_CONST'.
2. Resolving Arguments
The second byte represents the raw argument index. Different opcodes
interact with different lookup tables in the code object. The
dis module determines the type of argument based on opcode
categories:
- Constants: Opcodes such as
LOAD_CONSTuse the argument byte as an index to access values inco_consts. - Local Variables: Opcodes such as
LOAD_FASTorSTORE_FASTuse the argument to index intoco_varnames. - Global/Attribute Names: Opcodes such as
LOAD_GLOBALorLOAD_ATTRuse the argument to index intoco_names. - Jump Targets: Control-flow instructions calculate relative or absolute bytecode offsets based on the argument to display the target instruction location.
When dis detects one of these opcodes, it fetches the
actual value or name from the appropriate tuple and formats it alongside
the raw argument.
3. Mapping Line Numbers
The module reads the co_linetable (or
co_lnotab in versions prior to Python 3.11) to determine
which line of source code generated the instruction. It checks the
current byte offset against this mapping to determine when a new line
begins.
Understanding the Disassembled Output
When dis.dis() outputs the results, it formats each
instruction into distinct columns:
1 0 LOAD_CONST 1 ('hello')
2 STORE_FAST 0 (message)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
The columns represent:
- Source Line Number (e.g.,
1): The line of the original Python script where the operation originates. It only appears on the first instruction generated by that line. - Instruction Offset (e.g.,
0,2,4,6): The byte index of the instruction withinco_code. Because instructions are two bytes each, these increment by 2 (or more ifEXTENDED_ARGor inline caches are present in newer versions). - Opcode Name (e.g.,
LOAD_CONST,STORE_FAST): The human-readable string mapped from the opcode byte. - Raw Argument (e.g.,
1,0): The literal numerical value of the argument byte. - Resolved Argument (e.g.,
('hello'),(message)): The human-readable interpretation derived bydisqueryingco_consts,co_varnames, orco_names.
By unpacking binary pairs into operation names and cross-referencing
argument indices with the code object’s internal metadata tables, the
dis module provides full transparency into how the Python
Virtual Machine executes code.