Python

How Python Executes Your Code: Bytecode and the PVM Explained

Understand what happens between your Python source code and the machine: bytecode compilation, the Python Virtual Machine, and why it matters for performance and debugging.

August 2026 6 min read 10 views 0 hearts

What Happens When You Hit "Run" in Python

You write print("Hello, world!"), hit Enter, and text appears on your screen. But what actually happened inside your computer between that keystroke and the output?

Most developers use Python for years without understanding the invisible layer between their code and the machine. That layer is bytecode — and it's simpler than you think.

The Two-Step Dance

Python doesn't go straight from your source code to machine instructions. Instead, it does something clever:

  1. Compiles your code into an intermediate representation
  2. Interprets that representation on a virtual machine

This is the same approach Java uses, but with a crucial difference: Python does both steps automatically, without you ever running a separate compiler.

Where Bytecode Lives

When you import a module, Python checks for a .pyc file in the __pycache__ directory. This file contains bytecode that's already been compiled. If the source file is newer than the .pyc file, Python recompiles. If not, it skips straight to execution.

Try this yourself with Pythonskillset's example script:

# demo.py
def greet(name):
    return f"Hello, {name}!"

print(greet("Python"))

Import it and Python will create __pycache__/demo.cpython-39.pyc. That .pyc file? Pure bytecode.

Reading the Invisible

You can see exactly what Python compiles your code into:

import dis

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

dis.dis(greet)

This outputs something like:

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

Each line is a bytecode instruction. Python has about 100 of these total. Your entire program breaks down into combinations of them.

The Python Virtual Machine

Here's where the magic happens. The Python Virtual Machine (PVM) is a loop that:

  1. Reads the current instruction
  2. Figures out what to do
  3. Executes it
  4. Moves to the next instruction

This loop runs about 100,000 times per second in a typical Python program. The PVM doesn't understand for loops or if statements — it only understands these simple instructions.

The Stack Machine

Python's PVM is a stack-based virtual machine. Imagine a plate stacker at a cafeteria:

  • You put values on top (PUSH)
  • You take values from top (POP)
  • Operations use values from the top

When Python evaluates 2 + 3, it:

  1. PUSH 2 onto the stack
  2. PUSH 3 onto the stack
  3. BINARY_ADD pops both, adds them, pushes result back

For variables, Python uses LOAD_FAST (local variables) or LOAD_GLOBAL (global variables). These instructions copy values from a lookup table onto the stack.

Why This Matters for Performance

Now you understand why Python can be slower than C:

  • C code compiles directly to machine instructions that the CPU runs natively
  • Python bytecode runs on an interpreter that translates each instruction

The PVM itself is written in C, so there's always a layer of abstraction. Each bytecode instruction might correspond to dozens of actual CPU instructions.

When You Should Care

Understanding bytecode helps you:

  • Debug mysterious performance — If you see LOAD_GLOBAL inside a tight loop, that's a red flag
  • Understand scoping rules — Why LEGB rule exists (Local, Enclosing, Global, Builtins)
  • Optimize intelligently — Knowing that local variable access is faster than global access

Example from Pythonskillset's benchmarking:

import timeit

def slow():
    import math
    return math.sqrt(16)

def fast():
    from math import sqrt
    return sqrt(16)

# fast() runs about 30% faster

The bytecode shows why: slow() uses LOAD_GLOBAL for math and then LOAD_ATTR for sqrt. fast() just uses LOAD_FAST.

The Full Picture

Your Python journey from source to execution:

  1. Lexing — Breaking source into tokens
  2. Parsing — Building an Abstract Syntax Tree
  3. Compilation — Converting AST to bytecode
  4. Execution — PVM runs the bytecode loop

You rarely need to think about steps 1-3. But when your code runs slower than expected, or when you hit a weird scoping bug, knowing the bytecode layer gives you superpowers.

Next time you write a Python script, remember: between your readable code and the silicon, there's a beautifully simple stack machine doing exactly what you told it to — one bytecode instruction at a time.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.