Mastering Python Data Structures and Logic
Python Execution Model
From Code to Action
When you run a Python script, it feels instantaneous. You type python my_script.py, and things happen. But what's really going on under the hood? Your computer's processor doesn't understand Python directly. It only understands machine code, a low-level language of binary instructions.
So, how does your high-level Python code get translated into something the CPU can execute? This process is managed by the Python interpreter. The most common implementation, the one you're likely using, is called CPython because it's written in the C programming language.
CPython follows a two-step process: first compiling your source code into a simpler, intermediate format, and then interpreting that format to run the program.
Let's break down that first step: compilation. When you run your script, CPython first translates your .py source file into a format called bytecode. Bytecode is a set of instructions that is more primitive than your Python code but still more abstract than the machine code a CPU runs. It's a portable, intermediate language designed specifically for Python.
# Example Python function
def greet(name):
message = "Hello, " + name
print(message)
# Python doesn't execute this directly.
# It first compiles it to bytecode.
This compilation step is a performance optimization. To avoid re-compiling the code every single time you run it, Python saves this bytecode in files with a .pyc extension. You've probably seen a __pycache__ directory appear in your project folders. That's where Python stores these compiled bytecode files. The next time you run the script, if the .py file hasn't changed, Python skips the compilation step and runs the .pyc file directly, saving a bit of startup time.
The Python Virtual Machine
So now we have bytecode. What reads it? This is where the second step, interpretation, comes in. The bytecode is executed by the Python Virtual Machine (PVM).
The PVM isn't a piece of hardware; it's a program, the core runtime engine of Python. Its job is to read the bytecode instructions one by one and execute the corresponding operations. Think of it as a perfectly obedient agent that follows the bytecode recipe you've given it, managing your program's memory and state as it goes. This process of reading and executing bytecode is often called the interpreter's "execution loop."
This model is what makes Python a so-called "interpreted" language. Unlike a "compiled" language like C++ or Rust, where the source code is translated all the way to native machine code before execution, Python performs this translation at runtime via the PVM. This offers greater platform independence—the same bytecode can run on any system with a compatible PVM—but can introduce performance overhead.
| Feature | Interpreted (Python) | Compiled (C++) |
|---|---|---|
| Translation | Code is translated to bytecode at runtime by an interpreter (PVM). | Code is translated to machine code before runtime by a compiler. |
| Execution | The PVM executes bytecode instructions one by one. | The CPU executes machine code directly. |
| Portability | Highly portable. .pyc files can run on any OS with a PVM. | Less portable. Must be compiled for each specific target architecture. |
| Performance | Generally slower due to the overhead of the PVM. | Generally faster as code is optimized and runs directly on hardware. |
The Global Interpreter Lock
There's one more crucial piece of the CPython execution model that affects performance, especially in concurrent applications: the Global Interpreter Lock, or GIL.
The GIL is a mutex—a type of lock—that prevents multiple native threads from executing Python bytecode at the exact same time. Even if you have a machine with multiple CPU cores, only one thread can be executing Python code at any given moment within a single process.
Think of the GIL as a talking stick in a meeting. Only the person (thread) holding the stick can speak (execute bytecode). To speak, another person must wait for the stick to be passed to them.
Why does this lock exist? Its primary purpose is to simplify memory management in CPython. Python uses a mechanism called reference counting to clean up objects from memory. The GIL ensures that this process is thread-safe, protecting against corruption without requiring complex and fine-grained locking mechanisms on all data structures. This makes CPython's design simpler and makes single-threaded performance faster.
However, the GIL has a significant consequence for concurrency. If you have a CPU-bound task (like complex mathematical calculations) that you try to speed up using threads, you won't see a performance gain on a multi-core machine. Because of the GIL, your threads will execute serially, not in parallel, on the available cores.
So, while Python's execution model involves an intermediate compilation step, its runtime behavior is defined by the PVM's interpretation of bytecode. And within CPython, the GIL plays a critical role in managing state, with important implications for how we design concurrent programs.
Ready to check your understanding?
What is the primary role of the Python Virtual Machine (PVM)?
Why does Python create a __pycache__ directory containing .pyc files?
Understanding this model helps explain why Python behaves the way it does, from its portability to its performance characteristics in multi-threaded applications.