dis module
The dis module disassembles Python bytecode — it converts the compiled bytecode that CPython actually executes into a human-readable format. When you want to understand what your code is really doing, or why something performs the way it does, bytecode is where you look.
Bytecode is CPython’s internal representation. The dis module reads bytecode defined in the interpreter and exposes it for analysis. This is a CPython-specific tool: other Python implementations (PyPy, MicroPython) may use different bytecode entirely.
dis.dis() — disassemble a function or code object
The main entry point:
import dis
def add_one(x):
return x + 1
dis.dis(add_one)
The bytecode output reads from left to right: the first column shows the source line number, followed by the instruction offset, the opcode name, and, when applicable, the argument value. In the output above, you can see that the function loads the argument, loads the constant one, applies the binary addition operation, and then returns the result to the caller.
1 RESUME 0
2 LOAD_FAST 0 (x)
LOAD_CONST 1 (1)
BINARY_OP 0 (+)
RETURN_VALUE
The output shows the bytecode instructions that CPython executes for the function: RESUME marks the function entry point in Python 3.11 and later, LOAD_FAST pushes the local variable onto the stack, LOAD_CONST pushes the integer constant, BINARY_OP performs the addition, and RETURN_VALUE sends the result back to the caller. This is the bytecode for a single function, but dis can also operate on much larger targets.
For a module or class, dis.dis() recursively disassembles all functions or methods:
dis.dis(sys) # disassembles all functions in the sys module
When you pass a module, dis walks every function and method defined in its namespace and prints the bytecode for each one, which produces substantial output for large standard library modules. This recursive traversal is useful for auditing entire modules at once, though the output can be overwhelming for modules with many functions.
If called with no argument, disassembles the last traceback:
dis.dis()
While dis.dis() gives you a printed dump of bytecode, it is not designed for programmatic analysis. If you need to inspect individual instructions, filter by opcode, or build tooling around bytecode, you will want the Bytecode and Instruction objects that the module provides. These classes give you structured access to every property of the compiled code.
dis.Bytecode — the bytecode analysis object
Bytecode wraps code for detailed inspection:
bc = dis.Bytecode(add_one)
for instr in bc:
print(instr.opname, instr.argval)
The Bytecode constructor accepts several keyword arguments that control how the disassembly is formatted. The first_line argument sets which line number to report as the first line in the output. The current_offset argument marks a specific instruction as the current one, displayed with an arrow marker. In Python 3.13 and later, show_caches displays inline cache entries used by the interpreter to specialize bytecode at runtime. The adaptive argument shows the specialized bytecode that CPython generates. The show_offsets argument includes instruction offsets, and show_positions, added in Python 3.14, includes source positions in the output. These formatting options give you fine-grained control over the disassembly display without changing the underlying analysis.
from_traceback() constructs a Bytecode object from a traceback and sets current_offset to the instruction that raised the exception:
try:
something_that_fails()
except Exception:
dis.dis(distb())
The Bytecode object provides a high-level view of an entire code block, but sometimes you need to work with individual instructions directly. The get_instructions() function gives you an iterator over each instruction as a named tuple, which is more convenient when you want to filter, transform, or analyze instructions one at a time.
dis.get_instructions() — iterate over instructions
Returns an iterator of Instruction named tuples:
for instr in dis.get_instructions(add_one):
print(instr.opname, instr.argrepr)
Each Instruction has these attributes:
| Attribute | Description |
|---|---|
opname | Human-readable opcode name (e.g., LOAD_FAST) |
opcode | Numeric opcode value |
arg / oparg | Numeric argument (or None) |
argval | Resolved argument value (e.g., a variable name) |
argrepr | Human-readable argument description |
offset | Bytecode index of this instruction |
starts_line | True if this starts a source line |
line_number | Source line number (or None) |
is_jump_target | True if another instruction jumps here |
jump_target | Target offset if this is a jump instruction |
cache_info | Cache entry information (3.13+) |
dis.code_info() and dis.show_code()
code_info() returns a formatted multi-line string with details about a code object:
print(dis.code_info(add_one))
The output includes the function name, argument counts broken out by kind, source file, and references to free and cell variables that would matter in a closure context. This structured summary is useful when you need a quick overview without parsing individual instruction objects.
Name: add_one
Filename: <stdin>
Argument count: 1
Positional-only arguments: 0
Kw-only arguments: 0
Non-default kwargs: 0
First line: 1
Free variables: ()
Cell variables: ()
...
While code_info() returns a string that you can capture and process, you often just want to see the output immediately. The show_code() function eliminates the extra step of calling print() and handles the file output for you, accepting an optional file object to redirect the output.
show_code() is a convenience wrapper that prints the same information to a file (or sys.stdout):
dis.show_code(add_one) # print to stdout
dis.show_code(my_function, file=f) # print to file object f
All the functions covered so far operate on code objects you explicitly provide. When an exception has already occurred and you want to see what was on the call stack, distb() takes a different approach: it pulls the traceback from the current exception context and identifies the specific instruction that triggered the failure.
dis.distb() — disassemble a traceback
Disassembles the top-of-stack function from a traceback, marking the failing instruction:
import traceback
dis.distb()
# same as: dis.distb(sys.exc_info()[2])
Once you have the bytecode in front of you, the next logical question is where each instruction came from in the original source. The findlinestarts() function bridges bytecode offsets and source line numbers, returning a sequence of pairs that map instruction positions back to the lines that produced them.
dis.findlinestarts() — line start offsets
Yields (offset, lineno) pairs for each line start in bytecode:
list(dis.findlinestarts(add_one.__code__))
# [(0, 1), (2, 2)]
Line numbers can be None in bytecode that does not map to source lines (added in 3.13).
While findlinestarts() maps offsets to source lines, findlabels() serves a different purpose: it identifies jump targets within the bytecode itself. Jump targets are offsets where conditional branches, loops, and exception handlers redirect execution flow. Knowing these targets is essential for reconstructing the control flow graph of a function from its bytecode.
dis.findlabels() — jump target offsets
Finds all bytecode offsets that are jump targets — places another instruction can jump to:
dis.findlabels(add_one.__code__)
# returns a list of offsets
With source lines and jump targets mapped out, you have the structure of the bytecode. The next question is how each instruction interacts with the evaluation stack — does it push values on, pop them off, or both? The stack_effect() function answers this by computing the net change in stack size for any given opcode.
dis.stack_effect() — compute stack effects
Computes how an opcode changes the stack depth. This is useful for understanding bytecode complexity:
dis.stack_effect(dis.opmap['LOAD_FAST']) # 1 (pushes one value)
dis.stack_effect(dis.opmap['POP_TOP']) # -1 (removes one value)
dis.stack_effect(dis.opmap['BINARY_OP'], oparg=0) # -1 (pops 2, pushes 1)
For simple opcodes like LOAD_FAST or POP_TOP, the stack effect is constant regardless of execution path. But conditional jump instructions have two possible stack effects depending on whether the branch is taken or not. The jump parameter lets you compute both scenarios.
The jump parameter controls whether to use the jump or non-jump stack effect for conditional jumps:
# For a JUMP_IF_TRUE instruction:
dis.stack_effect(opcode, oparg, jump=True) # stack if jumping
dis.stack_effect(opcode, oparg, jump=False) # stack if not jumping
All of the functions described above are meant to be used from within a Python program, but you can also invoke the disassembler directly from the terminal. The command-line interface provides quick access without writing a script, with flags that mirror the keyword arguments available in the Python API.
Command-Line Interface
dis can be run as a script:
python -m dis mymodule.py # disassemble a file
python -m dis -O mymodule.py # show instruction offsets
python -m dis -C mymodule.py # show inline caches (3.13+)
python -m dis -S mymodule.py # show specialized bytecode (3.14+)
python -m dis -P mymodule.py # show source positions (3.14+)
Common bytecode operations
Knowing a few opcodes helps you read dis output:
LOAD_FAST/LOAD_CONST— push a local or constant onto the stackSTORE_FAST— pop the top of stack into a localCALL— call a functionBINARY_OP— perform a binary operation (+,-, etc.)COMPARE_OP— perform a comparisonJUMP_IF_TRUE/JUMP_IF_FALSE— conditional jumpsPOP_JUMP_IF_TRUE/POP_JUMP_IF_FALSE— pop and jumpRETURN_VALUE— return from a functionRESUME— marks the start of a function (3.11+)
See Also
- python-bytecode — understanding
.pycfiles and how Python compiles source to bytecode - debugging-with-pdb — using the Python debugger, which builds on inspection utilities
- python-gil-explained — using bytecode analysis to reason about Python’s thread model