Python Bytecode: A Beginner’s Guide

Emmanuel OyiboEmmanuel Oyibo
8 min read

Python bytecode is like a secret language that Python uses behind the scenes. When you write your Python code, it doesn’t run directly. Instead, Python translates your code into bytecode, a set of instructions that the Python interpreter can understand and execute.

You may be asking why beginners should care about bytecode. Well, understanding bytecode helps you peek under the hood of Python and see how your code works. This knowledge can help you write better, more efficient programs. Even if you don’t see bytecode directly, it’s a crucial part of making Python run smoothly.

In this guide, we’ll unravel the mystery of Python bytecode and show you why it matters.

What is Python Bytecode?

Python bytecode is like a middleman between your Python code and your computer’s hardware. When you write Python code and run it, the interpreter first translates your code into bytecode.

This bytecode is a lower-level representation of your code, but it’s still not something that your computer’s processor can understand directly.

That’s where the Python Virtual Machine (PVM) comes in. The PVM is like a special engine that’s designed to run bytecode. It reads the bytecode instructions one by one and carries them out, making your Python program come to life.

Benefits of Bytecode

Bytecode has a couple of benefits to you, the user. Let’s have a look at a couple of them:

  • Portability: Bytecode isn’t tied to any specific computer architecture, so the same bytecode can run on different types of machines.
  • Efficiency: Bytecode is often faster to execute than the original Python code. Python saves the bytecode in .pyc files. These files are like cached versions of your code. The next time you run the same program, Python can skip the compilation step and load the bytecode directly, making your program start up faster.

Therefore, you can think of bytecode as a bridge between your Python code and the inner workings of your computer. It’s a crucial part of the Python interpreter’s job, helping your code run smoothly and efficiently.

The Compilation Process

When you write Python code, it starts as a simple text file with a .py extension. But your computer doesn’t exactly understand this text directly. That’s where the compilation process comes in.

Now, let’s explore how compilation works:

  1. Source Code: You write your Python program in a plain text file, like my_program.py.
  2. Compilation: When you run your program, the Python interpreter gets to work. It reads your source code and translates it into bytecode, a lower-level representation of your code that’s more efficient for the computer to handle. This bytecode gets saved in a separate file with a .pyc extension (e.g., my_program.pyc).
  3. Execution: Now that the bytecode is ready, the Python Virtual Machine (PVM) steps in. The PVM is like a special engine that understands bytecode. It reads the bytecode instructions one by one and executes them.

In a nutshell, the compilation process converts your human-readable code into something your computer can understand and execute more efficiently.

Viewing Python Bytecode

Python provides a powerful tool called the dis module (short for “disassembler”) to unveil the bytecode behind your code. This module lets you disassemble Python functions or even entire scripts, revealing the low-level instructions that the Python interpreter executes.

Using dis.dis()

Let’s start with a simple function:

>>> def greet(name):
...     return f"Hello, {name}!"

To see the bytecode for this function, we use the dis.dis() function:

>>> import dis
>>> dis.dis(greet)

Output:

  1           0 RESUME                   0

  2           2 LOAD_CONST               1 ('Hello, ')
              4 LOAD_FAST                0 (name)
              6 FORMAT_VALUE             0
              8 LOAD_CONST               2 ('!')
             10 BUILD_STRING             3
             12 RETURN_VALUE

Now, let’s break down what these instructions mean:

  • RESUME 0: Marks the start of bytecode execution (specific to Python 3.11 and coroutines).
  • LOAD_CONST 1 ('Hello, '): Loads the string 'Hello, ' onto the stack.
  • LOAD_FAST 0 (name): Loads the local variable name onto the stack.
  • FORMAT_VALUE 0: Formats the value name.
  • LOAD_CONST 2('!'): Loads the string '!' onto the stack.
  • BUILD_STRING 3: Combines the three top stack values (’Hello, ‘, formatted name, '!') into one string.
  • RETURN_VALUE: Returns the combined string from the stack.

This sequence shows how Python builds and returns the final formatted string in the greet function.

Disassembling a Script

You can also disassemble an entire script. Let’s consider a simple example:

# File: example.py

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

def main():
        result = add(3, 4)
        print(f"The result is {result}")

if __name__ == "__main__":
        main()

Now, in a separate script, you can disassemble it as follows:

import dis
import example

dis.dis(example.add)
dis.dis(example.main)

You’ll get the bytecode for both functions, revealing the underlying instructions for each step.

Common Bytecode Instructions

Here are some of the most common bytecode instructions you’ll encounter, along with explanations and examples:

  • LOAD_CONST: loads a constant value (like a number, string, or None) onto the top of the stack.

    For example, LOAD_CONST 1 ('Hello, ') loads the string “Hello, “ onto the stack.

  • LOAD_FAST: loads the value of a local variable onto the stack.

    Example: LOAD_FAST 0 (x) loads the value of the local variable x.

  • STORE_FAST: takes the value on the top of the stack and stores it in a local variable.

    For example, STORE_FAST 1 (y) stores the top stack value into the variable y.

  • BINARY_ADD: takes the top two values from the stack, adds them together, and pushes the result back onto the stack.

    For example, In the sequence LOAD_FAST 0 (x), LOAD_CONST 1 (5), BINARY_ADD, the values of x and 5 are added, and the result is placed on the stack.

  • POP_TOP: removes the top value from the stack, effectively discarding it.

  • RETURN_VALUE: returns the topmost stack value, effectively ending the function’s execution.
  • JUMP_IF_FALSE_OR_POP: if the value at the top of the stack is false, this instruction jumps to a specified instruction. Otherwise, it pops the value from the stack.
  • JUMP_ABSOLUTE: jumps to a specific instruction, regardless of any condition.

Bytecode Examples for Basic Python Constructs

Let’s see how these instructions are used in basic Python constructs:

Conditional (If-Else)

def check_positive(x):
    if x > 0:
        return "Positive"
    else:
        return "Non-positive"

Bytecode:

2           0 LOAD_FAST                0 (x)
            2 LOAD_CONST               1 (0)
            4 COMPARE_OP               4 (>)
            6 POP_JUMP_IF_FALSE       14

3           8 LOAD_CONST               2 ('Positive')
           10 RETURN_VALUE

5     >>   12 LOAD_CONST               3 ('Non-positive')
           14 RETURN_VALUE

In the bytecode above:

  • LOAD_FAST 0 (x): Loads the variable x onto the stack.
  • LOAD_CONST 1 (0): Loads the constant 0 onto the stack.
  • COMPARE_OP 4 (>): Compares the top two stack values (x > 0).
  • POP_JUMP_IF_FALSE 14: Jumps to instruction 14 if the comparison is false.
  • LOAD_CONST 2 ('Positive'): Loads the string 'Positive' onto the stack if x > 0.
  • RETURN_VALUE: Returns the value on the stack.
  • LOAD_CONST 3 ('Non-positive'): Loads the string 'Non-positive' onto the stack if x <= 0.

Loops (For Loop)

def sum_list(numbers):
    total = 0
    for num in numbers:
        total += num
    return total

Bytecode:

2           0 LOAD_CONST               1 (0)
            2 STORE_FAST               1 (total)

3           4 LOAD_FAST                0 (numbers)
            6 GET_ITER
        >>   8 FOR_ITER                12 (to 22)
           10 STORE_FAST               2 (num)

4          12 LOAD_FAST                1 (total)
           14 LOAD_FAST                2 (num)
           16 INPLACE_ADD
           18 STORE_FAST               1 (total)
           20 JUMP_ABSOLUTE            8
        >>  22 LOAD_FAST                1 (total)
           24 RETURN_VALUE

Now, let’s explore what’s happening in the bytecode:

  1. LOAD_CONST 1 (0): Loads the constant 0 onto the stack to initialize total.
  2. STORE_FAST 1 (total): Stores 0 in the variable total.
  3. LOAD_FAST 0 (numbers): Loads the variable numbers onto the stack.
  4. GET_ITER: Gets an iterator for numbers.
  5. FOR_ITER 12 (to 22): Iterates over numbers, jumping to instruction 22 when done.
  6. STORE_FAST 2 (num): Stores the current item in the variable num.
  7. LOAD_FAST 1 (total): Loads total onto the stack.
  8. LOAD_FAST 2 (num): Loads num onto the stack.
  9. INPLACE_ADD: Adds total and num (in-place).
  10. STORE_FAST 1 (total): Stores the result back in total.
  11. JUMP_ABSOLUTE 8: Jumps back to the start of the loop.
  12. LOAD_FAST 1 (total): Loads total onto the stack.
  13. RETURN_VALUE: Returns total.

Understanding these common instructions and how they are used in different Python constructs can significantly enhance your ability to analyze bytecode and gain deeper insights into the inner workings of Python.

Conclusion

Python bytecode is the hidden language that makes your Python program run. It’s a lower-level representation of your code that the Python interpreter understands and executes. Bytecode is generated from your source code through a compilation process and stored in .pyc files for faster execution in future runs.

You can use the dis module to view and analyze bytecode, gaining insights into how Python translates your code into instructions.

By understanding common bytecode instructions and their role in basic Python constructs like loops and conditionals, you can optimize your code for better performance.


Thanks for reading! If you found this article helpful (which I bet you did 😉), got a question or spotted an error/typo... do well to leave your feedback in the comment section.

And if you’re feeling generous (which I hope you are 🙂) or want to encourage me, you can put a smile on my face by getting me a cup (or thousand cups) of coffee below. :)

Also, feel free to connect with me via LinkedIn.

10
Subscribe to my newsletter

Read articles from Emmanuel Oyibo directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Emmanuel Oyibo
Emmanuel Oyibo

As a budding DevOps engineer and a detail-oriented technical writer, I tackle the ever evolving realm of system automation, deployment, and integration on a regular basis. This blog is my online space where I document my journey, share the interesting things I discover, and untangle the challenging issues I face. My mission is to break down complex technical topics and make them straightforward and engaging. Whether you're deeply involved in tech or just starting to get curious, you're welcome here in my digital nook!