The dis module disassembles Python bytecode, revealing what the interpreter actually executes. Essential for understanding performance, debugging, and learning Python internals.

Basic Disassembly

import dis
 
def add(a, b):
    return a + b
 
dis.dis(add)
#   2           0 LOAD_FAST                0 (a)
#               2 LOAD_FAST                1 (b)
#               4 BINARY_ADD
#               6 RETURN_VALUE

Understanding the Output

  2           0 LOAD_FAST                0 (a)
  ^           ^    ^                     ^   ^
  |           |    |                     |   |
  line#    offset  opcode             arg  (name)
  • Line number: Source code line
  • Offset: Byte offset in bytecode
  • Opcode: The instruction
  • Argument: Instruction argument
  • Name: Human-readable argument interpretation

Comparing Implementations

import dis
 
# String concatenation
def concat_plus():
    return "hello" + " " + "world"
 
def concat_join():
    return " ".join(["hello", "world"])
 
def concat_fstring():
    return f"hello world"
 
print("=== Plus ===")
dis.dis(concat_plus)
 
print("\n=== Join ===")
dis.dis(concat_join)
 
print("\n=== F-string ===")
dis.dis(concat_fstring)

Loop Optimization

import dis
 
def loop_range():
    total = 0
    for i in range(100):
        total += i
    return total
 
def loop_while():
    total = 0
    i = 0
    while i < 100:
        total += i
        i += 1
    return total
 
dis.dis(loop_range)
# Uses GET_ITER and FOR_ITER (optimized)
 
dis.dis(loop_while)
# Uses COMPARE_OP and POP_JUMP_IF_FALSE (more instructions)

Bytecode Object

import dis
 
def example(x):
    if x > 0:
        return x * 2
    return 0
 
# Access bytecode directly
code = example.__code__
 
print(f"Name: {code.co_name}")
print(f"Arg count: {code.co_argcount}")
print(f"Local vars: {code.co_varnames}")
print(f"Constants: {code.co_consts}")
print(f"Bytecode: {code.co_code.hex()}")
 
# Get Bytecode object
bytecode = dis.Bytecode(example)
for instr in bytecode:
    print(f"{instr.offset:4d} {instr.opname:20} {instr.argrepr}")

Common Opcodes

import dis
 
# LOAD operations
def loads():
    x = 1           # LOAD_CONST, STORE_FAST
    y = x           # LOAD_FAST
    z = len         # LOAD_GLOBAL
    return z(y)     # CALL_FUNCTION
 
# Attribute access
def attrs():
    import os
    return os.path  # LOAD_ATTR
 
# Comparisons
def compare(a, b):
    return a < b    # COMPARE_OP
 
# Subscript
def subscript(lst):
    return lst[0]   # BINARY_SUBSCR

Jump Instructions

import dis
 
def conditional(x):
    if x:
        return 1
    else:
        return 0
 
dis.dis(conditional)
#   2           0 LOAD_FAST                0 (x)
#               2 POP_JUMP_IF_FALSE       8
#   3           4 LOAD_CONST               1 (1)
#               6 RETURN_VALUE
#   5     >>    8 LOAD_CONST               2 (0)
#              10 RETURN_VALUE

Exception Handling

import dis
 
def with_try():
    try:
        risky()
    except ValueError:
        handle()
    finally:
        cleanup()
 
dis.dis(with_try)
# Shows SETUP_FINALLY, POP_EXCEPT, etc.

List Comprehension vs Loop

import dis
 
def use_loop():
    result = []
    for i in range(10):
        result.append(i * 2)
    return result
 
def use_comprehension():
    return [i * 2 for i in range(10)]
 
# Comprehension creates a separate code object
dis.dis(use_comprehension)
# Shows MAKE_FUNCTION for the inner comprehension

Instruction Analysis

import dis
import sys
 
def analyze_function(func):
    """Analyze bytecode of a function."""
    bytecode = dis.Bytecode(func)
    
    stats = {
        'total_instructions': 0,
        'loads': 0,
        'stores': 0,
        'jumps': 0,
        'calls': 0,
    }
    
    for instr in bytecode:
        stats['total_instructions'] += 1
        
        if instr.opname.startswith('LOAD'):
            stats['loads'] += 1
        elif instr.opname.startswith('STORE'):
            stats['stores'] += 1
        elif 'JUMP' in instr.opname:
            stats['jumps'] += 1
        elif 'CALL' in instr.opname:
            stats['calls'] += 1
    
    return stats
 
def example(items):
    total = 0
    for item in items:
        if item > 0:
            total += item
    return total
 
print(analyze_function(example))

Code Object Attributes

def example(a, b, c=10):
    x = a + b
    y = x * c
    return y
 
code = example.__code__
 
# Important attributes
print(f"co_name: {code.co_name}")           # Function name
print(f"co_argcount: {code.co_argcount}")   # Positional args
print(f"co_varnames: {code.co_varnames}")   # Local variables
print(f"co_names: {code.co_names}")         # Global names
print(f"co_consts: {code.co_consts}")       # Constants
print(f"co_stacksize: {code.co_stacksize}") # Max stack depth
print(f"co_nlocals: {code.co_nlocals}")     # Number of locals

Disassemble String

import dis
 
# Disassemble code string
dis.dis("x = 1 + 2")
#   1           0 LOAD_CONST               0 (3)
#               2 STORE_NAME               0 (x)
#               4 LOAD_CONST               1 (None)
#               6 RETURN_VALUE
 
# Note: 1 + 2 is constant-folded to 3!

Show Code Info

import dis
 
def example(a, b):
    """Example function."""
    return a + b
 
dis.show_code(example)
# Name:              example
# Filename:          <stdin>
# Argument count:    2
# Positional-only:   0
# Kw-only arguments: 0
# Number of locals:  2
# Stack size:        2
# Flags:             OPTIMIZED, NEWLOCALS, NOFREE
# Constants:         0: None
# Variable names:    0: a, 1: b

Performance Insights

import dis
 
# Attribute access in loop
def slow():
    import math
    total = 0
    for i in range(1000):
        total += math.sqrt(i)  # LOAD_GLOBAL + LOAD_ATTR each iteration
    return total
 
def fast():
    from math import sqrt
    total = 0
    for i in range(1000):
        total += sqrt(i)  # Just LOAD_GLOBAL (faster)
    return total
 
# Compare bytecode in the loop

Common Opcode Reference

OpcodeDescription
LOAD_FASTLoad local variable
LOAD_GLOBALLoad global/builtin
LOAD_CONSTLoad constant
STORE_FASTStore to local
BINARY_ADDAddition
COMPARE_OPComparison
POP_JUMP_IF_FALSEConditional jump
CALL_FUNCTIONFunction call
RETURN_VALUEReturn from function
GET_ITERGet iterator
FOR_ITERIterate

Practical: Detect Recursion

import dis
 
def is_recursive(func):
    """Check if function calls itself."""
    code = func.__code__
    bytecode = dis.Bytecode(func)
    
    for instr in bytecode:
        if instr.opname == 'LOAD_GLOBAL' and instr.argval == func.__name__:
            return True
    return False
 
def factorial(n):
    if n <= 1:
        return 1
    return n * factorial(n - 1)
 
def iterative_sum(n):
    return sum(range(n))
 
print(is_recursive(factorial))      # True
print(is_recursive(iterative_sum))  # False

The dis module reveals Python's execution model. Use it to understand performance characteristics, verify optimizations, and learn how Python really works under the hood.

React to this post: