How Python Actually Runs Your Code Behind the Scenes

Most tutorials skip this. Here is exactly how Python runs your code, step by step, from a text file to bytecode running on the Python Virtual Machine.

Pythonbeginner
8 min read

When you type python and the name of a script into a terminal, a lot happens before the first line of output appears. Most beginner tutorials skip this story. They show the result and move on. But understanding how Python runs code makes you a faster debugger and a clearer thinker about performance. This guide walks through every step the Python interpreter takes, from the text file you saved to the bytes executing on your processor.

What Running Python Actually Means

People often call Python an interpreted language. That word makes it sound like Python reads your file character by character and runs each line as it goes. The reality is more interesting. Python is compiled first and interpreted second, in a two-stage process you never see directly.

When you run a script, the interpreter loads the file, parses it into a tree structure, compiles that tree into a compact instruction format called bytecode, and then hands the bytecode to a small virtual machine that executes one instruction at a time.

That virtual machine, the Python Virtual Machine, is the part doing the actual interpreting. It is a stack-based engine written in C. Every Python program on your computer, from a tiny script to PyTorch training a neural network, ultimately reduces to bytecode running on this same machine.

If you have not set up Python yet on your system, follow our guide to install Python on Windows, macOS, or Linux before continuing with the rest of this walkthrough.

Step 1: From Source Code to Bytecode

The first thing Python does with your file is read it as plain text. The lexer breaks that text into tokens such as keywords, names, numbers, and operators. The parser then arranges those tokens into an Abstract Syntax Tree, often shortened to AST, which represents the logical structure of your program.

From the AST, Python compiles a smaller, machine-friendly format called bytecode. Bytecode is not raw CPU machine code. It is a sequence of high-level instructions that the Python Virtual Machine understands. You can inspect the bytecode of any function using the standard library disassembly module:

pythonpython
import dis
 
def add(a, b):
    return a + b
 
dis.dis(add)

That short script prints six or seven instructions. You will see one to load each argument onto an internal stack, one to perform the addition, and one to return the result. These are the tiny steps the virtual machine executes when you call the function with two numbers, and they map directly to the operations described later in this tutorial.

Python caches this bytecode in hidden cache folders next to your source files so it does not have to recompile every run. When you change the source, the next run regenerates the cache automatically. You almost never interact with these files directly, and they are safe to delete whenever they appear in your project tree.

Step 2: The Python Virtual Machine

Once bytecode exists, the Python Virtual Machine starts a loop. It reads one instruction, executes it, and moves to the next. This loop is called the evaluation loop, and it lives inside a single very large C function in the CPython source code. That one function is the literal heart of every Python program ever written.

The virtual machine uses a value stack to do its work. Operations push values onto the stack, perform arithmetic or attribute lookups on those values, and pop the results back. Adding two variables becomes three stack operations: push the first variable, push the second, replace the top two with their sum.

This stack-based approach is part of why Python feels uniform across platforms. The same bytecode runs on Windows, Linux, and macOS because the virtual machine smooths over operating system differences. Your script does not care what hardware sits underneath, and that consistency is a real engineering win.

Want to see your first real script with this pipeline in mind? Walk through our guide on the first Python program explained line by line to connect these abstract steps to code you already understand.

Why CPython Is Slower Than C

Every Python instruction costs more than a single CPU instruction. The interpreter has to check types at runtime, manage reference counts for memory, look up names in dictionaries, and dispatch to the right C function for each operation. A C program skips all of that because the compiler knows everything before the program even runs.

This is why a plain Python loop summing one million integers runs noticeably slower than the same loop in C. It is also why libraries like NumPy and Pandas exist. They push the heavy work down into C extensions, leaving Python to orchestrate the high-level logic while the fast inner loops run in native code.

Python 3.13 introduced an experimental Just-In-Time compiler and an opt-in free-threaded build that removes the Global Interpreter Lock. Both target the same goal: less interpreter overhead per instruction. Future versions will keep narrowing the gap with compiled languages, and the changes are already visible in real benchmarks.

If performance ever starts hurting your project, the right reflex is not to abandon Python. It is to find the hot spot, push that one part into a faster layer, and keep the rest of your code in plain Python where iteration stays fast. For an isolated workspace where you can experiment with bytecode and benchmarks safely, set up a clean Python virtual environment first.

Frequently Asked Questions

Is Python compiled or interpreted?

Both. Python compiles your source code into bytecode first, then the Python Virtual Machine interprets that bytecode at runtime. This hybrid model gives you the readability of an interpreted language with some of the structural benefits of a compiled one. The compilation step is invisible to you, which is why people often describe Python as purely interpreted in casual conversation.

What is the difference between CPython and Python?

CPython is the reference implementation of the Python language, written in C. When you download Python from the official site, you are installing CPython. Other implementations exist, including PyPy with a tracing JIT, Jython on the Java Virtual Machine, and IronPython on the dotnet runtime. CPython is the one almost every production system in the world uses today.

Why does Python create hidden cache folders in my project?

Python stores compiled bytecode in a cache directory so it can skip recompilation on the next run. These folders are safe to delete. Python will regenerate them automatically whenever the source changes. Most projects add the cache folder to their ignore list so it never appears in version control, and that is the standard convention for every modern repository. ### Key Takeaways - Python compiles your source into bytecode and then runs that bytecode on the Python Virtual Machine. - The standard disassembly module lets you inspect the exact instructions the virtual machine will execute for any function. - Per-instruction interpreter overhead is the real reason CPython runs slower than C, not the language design itself. - Python 3.13 ships an experimental JIT and a free-threaded build that directly target this overhead. - Knowing the pipeline turns vague performance complaints into concrete debugging steps you can take today.

Conclusion

Python is not as magical as it looks. Your file goes through a tokeniser, a parser, a compiler, and finally a virtual machine. Every step is open source. Every step is inspectable. Knowing how Python runs code in practice turns "the interpreter did something weird" into a concrete question you can answer with a few lines of disassembly and a quick read of the CPython source. The next time someone says Python is slow, you can answer with the real reason: per-instruction overhead in the evaluation loop, not the language design itself. And when you sit down to optimise a real program, you will already be thinking in the right vocabulary. The natural next stop on this learning path is the role of Python variables and how names bind to objects, which is where the stack operations from this article start to feel intuitive in everyday code.