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.
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:
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?
What is the difference between CPython and Python?
Why does Python create hidden cache folders in my project?
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.
More in this topic
Python Dictionary Comprehensions Explained with Examples
A practical beginner guide to Python dictionary comprehensions. Learn the syntax, the filter clause, the inversion pattern, and when to reach for a regular loop instead.
Python List Comprehensions Explained Step by Step
A step by step beginner guide to Python list comprehensions. Learn the shape, the filter clause, the nested form, and when to reach for a regular loop instead.
Python *args and **kwargs Explained the Easy Way
A clear beginner guide to Python *args and **kwargs. Learn what the stars do, how to use both in function signatures, and the patterns that make flexible functions readable.