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.pyCurrent options include:
-Cto show inline caches;-Oto show offsets;-Pto show source positions;-Sto 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 totalDisassembly 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 operationsAvoid 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_codedirectly. - 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.







