Python dis Module and Bytecode — Core Concepts

Why bytecode inspection matters

CPython compiles Python source code into bytecode before executing it. This bytecode is what the CPython virtual machine actually runs. Understanding it helps you reason about performance differences between equivalent code, debug confusing behavior, and understand how Python features are implemented.

Reading dis output

The simplest way to use the module is calling dis.dis() on a function:

import dis

def add(a, b):
    return a + b

dis.dis(add)

This produces output like:

  2           0 LOAD_FAST                0 (a)
              2 LOAD_FAST                1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE

Each line shows: source line number, byte offset, instruction name, argument (if any), and a human-readable interpretation in parentheses.

Key bytecode instructions

LOAD_FAST — pushes a local variable onto the stack. The number is the index in the local variable table. This is faster than LOAD_GLOBAL or LOAD_NAME because locals are stored in a C array, not a dictionary.

STORE_FAST — pops the top of the stack and stores it in a local variable.

LOAD_CONST — pushes a constant value (numbers, strings, None, tuples of constants).

CALL_FUNCTION / CALL — calls a function with arguments from the stack. Python 3.12 simplified this to a single CALL instruction.

BINARY_ADD, BINARY_MULTIPLY, COMPARE_OP — arithmetic and comparison operations that pop two values and push the result.

JUMP_IF_FALSE_OR_POP, POP_JUMP_IF_FALSE — conditional jumps used by if statements, and/or operators, and loops.

The stack machine model

CPython’s VM is a stack machine. Values are pushed onto a stack, operations pop their inputs and push results. Understanding this explains why bytecode reads bottom-up for expressions:

For result = a + b * c:

  1. LOAD_FAST a — push a
  2. LOAD_FAST b — push b
  3. LOAD_FAST c — push c
  4. BINARY_MULTIPLY — pop b and c, push b*c
  5. BINARY_ADD — pop a and bc, push a + bc
  6. STORE_FAST result — pop and store

Comparing code approaches

A classic example — list comprehension vs. loop:

# Approach 1: list comprehension
def squares_comp(n):
    return [x * x for x in range(n)]

# Approach 2: explicit loop
def squares_loop(n):
    result = []
    for x in range(n):
        result.append(x * x)
    return result

Disassembling both reveals that the comprehension avoids LOAD_ATTR (for result.append) and CALL_FUNCTION on every iteration. The comprehension uses a specialized LIST_APPEND instruction that directly appends to the list being built, skipping the method lookup overhead.

The code object

Every function has a __code__ attribute (a code object) that holds the bytecode and metadata:

  • co_code / co_code — the raw bytecode bytes
  • co_consts — tuple of constants used in the function
  • co_names — tuple of names (global variables, attributes)
  • co_varnames — tuple of local variable names
  • co_stacksize — maximum stack depth needed

The dis module is essentially a pretty-printer for code objects.

Common misconception

People sometimes think bytecode optimization means their code runs faster in later Python versions because the bytecode is “better.” While CPython does add new instructions (like LOAD_FAST_AND_CLEAR in 3.12), most performance improvements come from the interpreter loop itself (computed gotos, specializing adaptive interpreter in 3.11+) rather than from generating different bytecode.

Bytecode changes across versions

Bytecode is not stable across Python versions. The instruction set changes regularly — Python 3.11 added specializing instructions, 3.12 reorganized call instructions, 3.13 introduced more micro-ops. Code that inspects raw bytecode bytes must be version-aware. The dis module abstracts most of these changes, which is why you should use it instead of parsing co_code directly.

The one thing to remember: The dis module decodes CPython’s stack-based bytecode into readable instructions, giving you concrete insight into what Python actually does with your code at the lowest level before machine code.

pythoninternalsdebugging

See Also

  • Python Ast Module Code Analysis How Python's ast module reads your code like a grammar teacher diagrams sentences — turning source text into a tree you can inspect and change.
  • Python Gc Module Internals How Python's garbage collector automatically cleans up memory you are no longer using — like a tidy roommate for your program.
  • Python Importlib Custom Loaders How Python's importlib lets you teach Python to load code from anywhere — databases, zip files, the internet, or even generated on the fly.
  • Python Site Customization How Python's site module sets up your environment before your code even starts running — the invisible first step of every Python program.
  • Python Startup Optimization Why Python takes a moment to start and what you can do to make your scripts and tools launch faster.