The dis module disassembles Python code objects and displays the bytecode instructions executed by the interpreter. It helps explain how expressions, functions, loops, comprehensions, exceptions, and calls are compiled. It is also useful for teaching, debugging tools, performance investigation, and comparing Python releases.
Bytecode is an implementation detail. Instructions, arguments, offsets, inline caches, and optimizations can change between releases, including minor versions. Never build business logic, persistent formats, or security boundaries around one exact instruction sequence unless the runtime version is strictly constrained and tested.
Disassemble a function
dis.dis() accepts functions, methods, classes, modules, source strings, and code objects.
import dis
def add(a, b):
return a + b
dis.dis(add)
The output shows offsets, instruction names, arguments, and useful operand representations.
Compile an expression
Dynamically compiled code can also be inspected.
code = compile("result = x * 2", "example.py", "exec")
dis.dis(code)
compile() does not execute the source, but untrusted source should still be handled inside resource limits.
Code objects
Functions expose a __code__ object containing bytecode, constants, names, variables, and metadata.
code = add.__code__
print(code.co_varnames)
print(code.co_consts)
print(code.co_names)
These fields are intended for introspection and can evolve.
Programmatic instructions
dis.get_instructions() returns Instruction objects and is more reliable than parsing formatted text.
for instruction in dis.get_instructions(add):
print(
instruction.offset,
instruction.opname,
instruction.arg,
instruction.argval,
)
Use named fields rather than columns from human output.
The Instruction object
An instruction can contain opcode name, numeric argument, resolved value, printable argument, offset, source positions, and jump-target information.
Not every field applies to every opcode. Handle missing values.
Bytecode class
dis.Bytecode provides an iterable interface and formatting helpers.
bytecode = dis.Bytecode(add)
for instruction in bytecode:
print(instruction.opname)
print(bytecode.dis())
This is useful when an analysis tool needs to keep configuration and code together.
Lines and source positions
Instructions can be associated with source lines and more precise source ranges. Modern Python stores detailed position metadata.
Use it for diagnostics, but accept missing positions in generated or transformed code.
Offsets
Disassembly options can display offsets explicitly. They help interpret jumps and exception tables.
Offsets are not stable identifiers and should not be compared across Python versions or builds.
Load instructions
Constants, names, globals, locals, and attributes use different load operations.
Compare global and local access:
x = 10
def use_global():
return x
def use_local(x):
return x
Bytecode visualizes the difference, but language-level scope rules remain the authoritative explanation.
Binary operations
Arithmetic expressions load operands and apply operations.
def calculate(a, b):
return (a + b) * 2
Modern Python may use one general instruction with an argument identifying the particular binary operation.
Constant folding
The compiler can evaluate selected constant expressions ahead of time.
def example():
return 2 + 3
Disassembly may contain only the final constant. Do not assume every apparently constant expression will always be folded.
Loops
A for loop normally obtains an iterator, requests successive values, and uses jumps.
def total(items):
result = 0
for item in items:
result += item
return result
Studying the flow helps teach iteration, although optimization details can change.
Conditions and jumps
if, while, boolean expressions, and short-circuit behavior use jumps.
is_jump_target identifies instructions that can receive control flow.
for instruction in dis.get_instructions(function):
if instruction.is_jump_target:
print("target", instruction.offset)
Relative jump details
Jump representation and offset units have changed across Python versions. Use argval and current APIs instead of manually decoding historical byte layouts.
Analysis tools should declare supported releases.
Function calls
Calls involve loading a callable and arguments plus instructions for preparation and execution.
The exact sequence varies for methods, keyword arguments, and adaptive specialization.
Comprehensions
List, set, and dictionary comprehensions can produce nested code objects.
def double_evens(values):
return [x * 2 for x in values if x % 2 == 0]
dis.dis() can display nested code objects, revealing the implicit function-like component.
Generators
Functions containing yield use instructions and flags for suspension and resumption.
Do not manipulate generator state through bytecode internals. Use public APIs.
async and await
Coroutines and asynchronous generators generate bytecode for waiting, sending, and resuming.
The form changes significantly between versions, so treat it as a study of the current runtime.
Exception handling
Exception handling uses metadata and instructions that have changed substantially through CPython’s evolution.
Do not search only for historical opcodes to identify try blocks. Use current metadata and APIs.
Exception tables
Modern code objects can store compact exception tables rather than placing all information directly in the instruction stream.
Disassembly output exposes useful details, but the internal encoding is not a stability contract.
Adaptive interpreter
CPython can specialize instructions at runtime according to observed object types. This improves performance without changing source code.
Disassembly can show original or specialized instructions depending on options and execution history.
adaptive option
Current APIs can display specialized instructions when available.
dis.dis(function, adaptive=True)
Run a function repeatedly before inspection if you want to observe warm specialization, but results need not be identical across systems.
show_caches
Inline caches can be displayed with show_caches=True.
dis.dis(function, show_caches=True)
These caches are performance internals. Never modify them or use them for functional decisions.
Runtime changes
Specialization means observed bytecode can differ after warm-up. A comparison tool must control whether adaptive code and caches are shown.
Record Python version, options, and execution count.
stack_effect
dis.stack_effect(opcode, oparg) calculates an instruction’s effect on the evaluation stack.
import dis
import opcode
effect = dis.stack_effect(opcode.opmap["LOAD_CONST"], 0)
print(effect)
Conditional jumps can require a branch-specific calculation.
Stack analysis
Adding stack effects linearly is insufficient in a graph containing jumps. Correct analysis propagates stack heights through basic blocks and verifies convergence.
Use a specialized verifier for serious bytecode validation.
opcode module
The opcode module provides maps and opcode categories.
import opcode
print(opcode.opmap.get("RETURN_VALUE"))
Numeric values change. Use names and maps from the current runtime.
Different Python implementations
dis describes bytecode supported by its runtime, especially CPython. Other implementations can use different execution representations.
Do not generalize CPython details into language guarantees.
Performance analysis
Bytecode can reveal repeated loads, calls inside loops, and allocation patterns. Still, visual inspection is not performance measurement.
Use timeit, cProfile, and representative workloads. One fewer instruction may have no meaningful impact.
Microbenchmarks
Compare alternatives with warm-up, repetition, isolation, and noise control.
Adaptive specialization makes steady-state measurement especially important.
Security limitations
The presence or absence of one opcode does not prove code safe or dangerous. Aliases, imports, object behavior, and dynamic features defeat simple allowlists.
Never create a sandbox by validating bytecode and then calling exec(). Use process isolation and a genuinely restricted language.
Untrusted code objects
Do not load code objects from untrusted marshal or pickle payloads into a critical process merely to disassemble them.
Even non-executing analysis should be isolated and resource-limited for hostile inputs.
Bytecode diffs
When comparing versions, normalize offsets, caches, and unstable fields. Compare instruction names and semantic arguments without demanding byte-for-byte identity.
Separate language changes from implementation-only differences.
Command line
The module can disassemble a source file through the command line.
python -m dis module.py
This is useful for quick inspection and learning.
Combine with AST
The AST shows high-level structure while dis shows post-compilation instructions. Comparing both layers helps explain compiler transformations.
See Python ast.
Combine with tokenize
tokenize preserves comments and source spelling, which do not survive into bytecode.
See Python tokenize for the lexical layer.
Testing analysis tools
Run the suite on every supported Python version. Include simple functions, closures, generators, async code, comprehensions, exceptions, and pattern matching.
Use version-specific fixtures when expected opcodes differ.
Common mistakes
Common failures include assuming opcodes are stable, parsing formatted disassembly text, comparing offsets across versions, ignoring adaptive caches, optimizing without measurement, treating bytecode validation as a sandbox, generalizing CPython to all implementations, and loading untrusted code objects.
Conclusion
dis shows how Python compiles and executes code at the bytecode level. Use get_instructions() for programmatic analysis, control cache and adaptive options, and always record the runtime version.
Treat bytecode as a moving implementation detail and profile before optimizing. Consult the official dis documentation and the opcode documentation.







