Python - Python Bytecode Inspection Using the dis Module

Python is a high-level programming language, but it does not execute source code directly. Before a Python program runs, the interpreter converts the source code into an intermediate form called bytecode. This bytecode consists of low-level instructions that the Python Virtual Machine (PVM) can understand and execute. Although bytecode is not machine code, it acts as a bridge between Python source code and the execution process.

The dis module is a built-in Python library that allows developers to inspect the bytecode generated from Python programs. By examining bytecode, programmers can understand how Python interprets their code, identify inefficiencies, debug performance issues, and gain a deeper understanding of the language's internal working.

What is Python Bytecode?

Bytecode is a sequence of instructions produced after Python source code is compiled. These instructions are platform-independent, meaning the same bytecode can run on different operating systems as long as they have a compatible Python interpreter.

For example, when you write:

x = 10
y = 20
print(x + y)

Python first compiles this into bytecode before executing it.

The bytecode instructions are stored in .pyc files inside the __pycache__ directory after a module is imported or compiled.

What is the dis Module?

The dis module stands for disassembler. It converts Python bytecode into a readable format, allowing developers to inspect the exact instructions executed by the Python Virtual Machine.

The module is included with every Python installation, so no external packages are required.

Importing the module is simple:

import dis

Basic Example of Bytecode Inspection

Consider the following function:

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

To inspect its bytecode:

import dis

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

dis.dis(add)

Possible output:

  2           0 RESUME                   0

  3           2 LOAD_FAST                0 (a)
              4 LOAD_FAST                1 (b)
              6 BINARY_OP                0 (+)
             10 RETURN_VALUE

This output represents the sequence of bytecode instructions executed by Python.

Understanding Common Bytecode Instructions

LOAD_FAST

Loads a local variable onto the evaluation stack.

Example:

x = 10

The variable x is loaded whenever it is needed.

LOAD_CONST

Loads a constant value.

Example:

a = 100

The number 100 is treated as a constant.

STORE_FAST

Stores a value into a local variable.

Example:

x = 50

The computed value is stored in the variable x.

LOAD_GLOBAL

Loads a globally defined object or function.

Example:

print("Hello")

The print function is loaded as a global object.

CALL

Executes a function call after preparing its arguments.

Example:

print("Python")

Python loads print, loads the argument, and then calls the function.

RETURN_VALUE

Returns the final value from a function.

Example:

def square(x):
    return x * x

The multiplication result is returned through the RETURN_VALUE instruction.

Inspecting Expressions

Example:

def calculate():
    x = 5
    y = 8
    return x * y

Disassembling:

import dis

dis.dis(calculate)

Python generates instructions to:

  • Load the constant 5

  • Store it in x

  • Load the constant 8

  • Store it in y

  • Load both variables

  • Multiply them

  • Return the result

Each step corresponds to one or more bytecode instructions.

Inspecting Conditional Statements

Example:

def compare(a, b):
    if a > b:
        return a
    return b

The bytecode contains comparison instructions and conditional jump operations.

Python internally creates instructions that:

  • Compare a and b

  • Jump to different instruction locations depending on the comparison result

  • Return the appropriate value

This shows how high-level if statements are translated into lower-level execution steps.

Inspecting Loops

Example:

def count():
    for i in range(3):
        print(i)

The bytecode contains instructions for:

  • Creating the iterator

  • Fetching the next value

  • Detecting when iteration ends

  • Calling the print() function

  • Repeating until completion

Although the Python code contains only one for loop, the bytecode consists of many individual instructions.

Inspecting Function Calls

Example:

def message():
    print("Welcome")

Disassembly reveals several operations:

  • Load the global print function

  • Load the string constant

  • Call the function

  • Return None

This demonstrates that even a simple function call involves multiple bytecode instructions.

Viewing Bytecode of Lambda Functions

Example:

square = lambda x: x * x

import dis
dis.dis(square)

The bytecode shows that lambda functions are compiled similarly to regular functions, with only minor structural differences.

Viewing Bytecode of List Comprehensions

Example:

numbers = [x * 2 for x in range(5)]

Python internally generates a separate code object for the list comprehension. The dis module can reveal how iteration, multiplication, and list creation are handled behind the scenes.

Inspecting Code Objects

Every Python function contains a code object accessible through the __code__ attribute.

Example:

def greet():
    print("Hello")

print(greet.__code__)

The code object stores important information such as:

  • Bytecode instructions

  • Constants

  • Variable names

  • Function arguments

  • Local variables

  • Line numbers

The dis module interprets this code object into readable bytecode.

Why Bytecode Inspection is Useful

Bytecode inspection provides valuable insights for developers by:

  • Understanding how Python executes source code.

  • Learning the internal behavior of loops, functions, and conditional statements.

  • Comparing different coding approaches to identify more efficient implementations.

  • Debugging unexpected behavior in complex programs.

  • Studying language internals for advanced Python development.

  • Improving code performance by recognizing unnecessary operations.

Limitations of Bytecode Inspection

While bytecode inspection is useful, it has some limitations:

  • Bytecode instructions may change between Python versions.

  • Bytecode is intended for understanding execution rather than modifying programs.

  • Reading bytecode requires familiarity with Python's execution model.

  • It does not directly show CPU machine instructions, as Python still relies on the Python Virtual Machine for execution.

Best Practices

  • Use the dis module primarily for learning, debugging, and performance analysis.

  • Compare bytecode for different implementations to understand execution differences.

  • Keep your Python interpreter updated, as newer versions may introduce optimized bytecode instructions.

  • Avoid writing code solely to produce fewer bytecode instructions; prioritize readability and maintainability unless profiling identifies a real performance bottleneck.

  • Combine bytecode inspection with profiling tools such as cProfile and timeit for a more complete understanding of performance.

Conclusion

The dis module is an essential tool for exploring how Python transforms source code into bytecode before execution. By examining bytecode, developers gain a deeper understanding of the Python Virtual Machine, function calls, loops, conditional statements, and other language constructs. Although bytecode inspection is mainly used for learning, debugging, and optimization, it offers valuable insights into Python's execution process and helps developers write more informed and efficient programs.