Python dis: Understand Bytecode

Published on: August 3, 2026
Reading time: 6 minutes
Monitor with binary code representing bytecode analysis with Python dis

Python source code is compiled into intermediate instructions executed by the interpreter’s virtual machine. In CPython, those instructions form bytecode. The Python dis module disassembles functions, methods, classes, generators, coroutines, and source snippets, revealing variable loads, calls, jumps, operators, adaptive caches, and the relationship between source positions and internal operations.

This guide covers dis.dis(), Bytecode, get_instructions(), code_info(), traceback analysis, and specialized bytecode. It complements our articles about slow Python scripts, cProfile, timeit, pdb, and inspect.

What bytecode is

When CPython loads a function, it compiles the syntax into a code object. That object contains constants, names, local-variable slots, flags, source positions, and a sequence of virtual-machine instructions.

def double(number):
    return number * 2

print(double.__code__)
print(double.__code__.co_consts)
print(double.__code__.co_varnames)

Bytecode is not processor machine code. It is an interpreter-specific representation consumed by CPython’s evaluation loop.

Your first disassembly

dis.dis() prints a function’s instructions.

import dis

def double(number):
    return number * 2

dis.dis(double)

Output may contain operations such as RESUME, LOAD_FAST, LOAD_CONST, BINARY_OP, and RETURN_VALUE. Names and details vary across releases.

Bytecode is an implementation detail

The official dis documentation explicitly states that CPython bytecode is an implementation detail. Instructions can be added, removed, or changed, and other Python virtual machines may use a different model.

Use disassembly for learning, diagnostics, compiler tests, and version-specific tools. Do not make business logic depend on opcodes as a portable contract.

Analyze strings and code objects

dis() also accepts source strings, modules, classes, methods, coroutine objects, and code objects returned by compile().

dis.dis("result = add(a, b)")

code = compile(
    "total = price * quantity",
    "example.py",
    "exec",
)
dis.dis(code)

For a class or module, the function can recursively visit nested code objects.

Control recursive depth

Nested functions, comprehensions, and generator expressions have their own code objects. The depth argument controls recursive disassembly.

def outer():
    def inner(value):
        return value + 1
    return [inner(x) for x in range(3)]

dis.dis(outer, depth=0)
dis.dis(outer, depth=2)

Use a small depth for large modules and classes so the report remains understandable.

Inspect code-object information

code_info() returns a formatted summary containing argument counts, variables, constants, names, flags, and stack size.

print(dis.code_info(outer))

show_code() prints equivalent information directly.

dis.show_code(outer)

These summaries provide context before instruction-level analysis.

The Bytecode class

dis.Bytecode wraps an analyzed object and supports formatted output or structured iteration.

bytecode = dis.Bytecode(double)

print(bytecode.info())
print(bytecode.dis())

for instruction in bytecode:
    print(instruction.opname, instruction.argrepr)

It accepts functions, methods, generators, coroutines, source strings, and code objects.

Structured analysis with get_instructions()

Tools that need to process operations rather than print text should use get_instructions().

for instruction in dis.get_instructions(double):
    print({
        "operation": instruction.opname,
        "offset": instruction.offset,
        "argument": instruction.argval,
        "line": instruction.line_number,
    })

Each Instruction exposes the numeric opcode, readable name, resolved argument, source position, jump target, base operation, and cache information.

LOAD and STORE operations

Operations beginning with LOAD place references or values on the evaluation stack. STORE removes a value and assigns it to a local, global, attribute, or container.

def example(value):
    result = value + 10
    return result

dis.dis(example)

LOAD_FAST accesses a local slot, LOAD_CONST loads a constant, and STORE_FAST writes to a local slot. Globals, attributes, and closures use different instructions.

The evaluation stack

Many instructions consume operands from the stack and push a result. For addition, the interpreter loads two values and executes a binary operation.

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

dis.dis(add)

dis.stack_effect() calculates the net stack change for an opcode.

opcode = dis.opmap["LOAD_CONST"]
print(dis.stack_effect(opcode, 0))

This function is useful in bytecode validators, simulators, and compiler-related tools.

Jumps and control flow

Conditions, loops, short-circuit expressions, pattern matching, and exception handling compile to comparisons and jumps.

def classify(number):
    if number >= 0:
        return "positive"
    return "negative"

dis.dis(classify, show_offsets=True)

Recent releases display logical labels for targets. Avoid manual offset arithmetic that ignores caches or release-specific jump semantics.

Source positions in Python 3.14

Python 3.14 adds show_positions=True and the -P command-line option. Disassembly can include start and end lines plus covered columns.

dis.dis(
    classify,
    show_offsets=True,
    show_positions=True,
)

Precise positions support debuggers, coverage tools, static analysis, and error messages that highlight a specific expression.

Use dis from the command line

The module can disassemble a file or source read from standard input.

python -m dis program.py

Current options include:

  • -C to show inline caches;
  • -O to show offsets;
  • -P to show source positions;
  • -S to show specialized bytecode.

Options depend on the installed Python version.

Inline caches

Since Python 3.11, selected instructions reserve cache entries used by interpreter specialization. Set show_caches=True to display them.

dis.dis(double, show_caches=True)

Cache data logically belongs to the preceding instruction. Do not interpret populated cache bytes as independent operations or modify raw adaptive bytecode.

Adaptive and specialized bytecode

CPython can specialize operations after observing runtime types and access patterns. Use adaptive=True, or -S in the Python 3.14 command-line interface.

for _ in range(20_000):
    double(10)

dis.dis(double, adaptive=True, show_caches=True)

Specialization is dynamic. Results may differ according to warm-up, build configuration, architecture, and process state.

Disassembly is not a benchmark

Fewer visible instructions do not guarantee better performance. One operation can call complex C code, allocate objects, or perform I/O. Confirm hypotheses with timeit, cProfile, and application metrics.

def using_sum(values):
    return sum(values)

def using_loop(values):
    total = 0
    for value in values:
        total += value
    return total

Disassembly explains structural differences, while benchmarks measure their effect.

Analyze a failure with distb()

dis.distb() disassembles the function at the top of a traceback and marks the instruction associated with the failure.

try:
    value = (1, 2)[5]
except IndexError as error:
    dis.distb(error.__traceback__)

Bytecode.from_traceback() provides a structured alternative.

bytecode = dis.Bytecode.from_traceback(error.__traceback__)
print(bytecode.dis())

This works well with our guide to Python traceback formatting.

Closures and free variables

Inner functions can access cells owned by an outer scope. Disassembly may show MAKE_CELL, LOAD_DEREF, and COPY_FREE_VARS.

def multiplier(factor):
    def apply(value):
        return value * factor
    return apply

dis.dis(multiplier)

These operations make closure storage and access easier to visualize.

Generators and coroutines

Generators, yield from, await, and async generators involve instructions such as RETURN_GENERATOR, YIELD_VALUE, SEND, and GET_AWAITABLE.

async def fetch(client):
    return await client.get()

dis.dis(fetch)

Async bytecode changes considerably between releases as the interpreter evolves.

Comprehensions

A list comprehension may compile to a nested code object or receive a release-specific optimization.

def doubles(limit):
    return [n * 2 for n in range(limit)]

dis.dis(doubles, depth=2)

Observe iteration, append, and call instructions, but do not rewrite a comprehension solely because one bytecode listing appears longer.

Opcode collections

The module exposes collections for automatic introspection.

print(dis.opmap["RETURN_VALUE"])
print(dis.opname[dis.opmap["RETURN_VALUE"]])
print(dis.hasconst)
print(dis.hasjump)

opmap maps names to numeric codes, while opname performs the reverse mapping. Collections such as hasconst, hasname, hasfree, and hasjump classify operations.

Testing bytecode

Interpreter-adjacent libraries sometimes verify an operation, but tests must be conditional on the runtime implementation and version.

import sys

operations = {
    item.opname
    for item in dis.get_instructions(double)
}

if sys.implementation.name == "cpython":
    assert "RETURN_VALUE" in operations

Avoid rigid text snapshots because labels, offsets, caches, and formatting change.

Do not import untrusted code casually

Disassembling a source string compiles without executing it, but analyzing a module may tempt a tool to import that module first. Imports execute top-level code. Analyze untrusted projects in an isolated process without credentials and with restricted permissions.

Common mistakes

  • Treating opcodes as a stable public API.
  • Comparing bytecode from different releases without context.
  • Inferring speed only from instruction counts.
  • Ignoring inline caches and specialization.
  • Building tools around old offset rules.
  • Importing untrusted modules for analysis.
  • Editing co_code directly.
  • Using fragile text snapshots in tests.

Best practices

  • Record the Python version and implementation.
  • Use get_instructions() for structured analysis.
  • Prefer positions and labels to fragile arithmetic.
  • Confirm performance ideas with benchmarks.
  • Limit recursion depth on large objects.
  • Analyze adaptive bytecode only after controlled warm-up.
  • Isolate unknown code.
  • Treat release-to-release changes as expected.

Conclusion

The Python dis module reveals how CPython translates language constructs into virtual-machine operations. It helps explain local variables, calls, conditions, loops, closures, generators, exceptions, caches, and adaptive specialization.

This view is powerful but tied to one implementation and release. Use disassembly as a learning and diagnostic tool, not as a portable contract or replacement for measurement. With Bytecode, get_instructions(), source positions, and controlled comparisons, you can investigate interpreter behavior without building systems on fragile opcode assumptions.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Error screen representing crash and deadlock diagnosis with Python faulthandler
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python faulthandler: Diagnose Crashes

    Learn Python faulthandler to diagnose crashes, deadlocks, and timeouts using thread dumps and native C stack information.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Laptop with code representing Python traceback analysis and debugging
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python traceback: Errors and Call Stacks

    Learn Python traceback to capture, format, and log error call stacks safely without leaking sensitive data or retaining memory.

    Ler mais

    Tempo de leitura: 6 minutos
    03/08/2026
    Software analysis representing object introspection with Python inspect
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python inspect: Object Introspection

    Learn Python inspect to analyze functions, classes, signatures, source code, decorators, generators, coroutines, and frames safely.

    Ler mais

    Tempo de leitura: 6 minutos
    02/08/2026
    Memory module representing weak references and caches in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python weakref: Weak References

    Learn Python weakref to build weak references, automatic caches, observer registries, and finalizers without retaining objects in memory.

    Ler mais

    Tempo de leitura: 8 minutos
    28/07/2026
    ZIP archive icon for a Python zipfile article
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipfile: Safe ZIP Archives

    Learn how to create, read, validate, and extract ZIP archives with Python zipfile in a predictable and secure workflow.

    Ler mais

    Tempo de leitura: 4 minutos
    27/07/2026
    Software dependency graph and Python task workflow
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python graphlib: Topological Sorting

    Learn Python graphlib to order dependencies, detect cycles, and coordinate independent tasks safely in parallel.

    Ler mais

    Tempo de leitura: 5 minutos
    27/07/2026