pyguides

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:

AttributeDescription
opnameHuman-readable opcode name (e.g., LOAD_FAST)
opcodeNumeric opcode value
arg / opargNumeric argument (or None)
argvalResolved argument value (e.g., a variable name)
argreprHuman-readable argument description
offsetBytecode index of this instruction
starts_lineTrue if this starts a source line
line_numberSource line number (or None)
is_jump_targetTrue if another instruction jumps here
jump_targetTarget offset if this is a jump instruction
cache_infoCache 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 stack
  • STORE_FAST — pop the top of stack into a local
  • CALL — call a function
  • BINARY_OP — perform a binary operation (+, -, etc.)
  • COMPARE_OP — perform a comparison
  • JUMP_IF_TRUE / JUMP_IF_FALSE — conditional jumps
  • POP_JUMP_IF_TRUE / POP_JUMP_IF_FALSE — pop and jump
  • RETURN_VALUE — return from a function
  • RESUME — marks the start of a function (3.11+)

See Also

  • python-bytecode — understanding .pyc files 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