What Actually Happens When You Run a Python Script
When you look at a Python script, it feels obvious what is going on. You see variables, functions and logic that makes sense to you.
But a computer does not see any of that. All it sees is a plain string of characters. Letters, numbers, spaces and newlines. It does not know that this text is "code", let alone what it is supposed to do.
What you see:
a = 2
b = 3
c = a + b
print(c)What the computer sees:
" a = 2 \n b = 3 \n c = a + b \n print(c) "The goal is to turn this string into something the machine can actually execute.
CPUs do not understand Python, JavaScript, or any other high-level language. They only understand machine code, sequences of 0s and 1s that map to very specific hardware instructions. These instructions are not universal either. An Intel x86 CPU speaks a different instruction set than an ARM CPU, which is why programs are often built specifically for the machine they will run on.
Traditional compiled languages lean into this. They are translated directly into machine instructions for a specific CPU. Python takes a different approach.
Python is often described as an interpreted language, but it does not run source code directly on the CPU. Instead, it first converts your code into a portable intermediate representation that sits between human-readable code and raw machine instructions. That representation can then be executed by the Python runtime on any machine where Python is installed. That process starts by turning raw text into structure.
Lexing
Lexing, or tokenising, is the step where CPython's compiler scans the raw stream of characters and groups it into tokens (like names, numbers, and operators) so later stages can work with structure instead of plain text.
This output shows the exact tokens produced by Python's lexer, demonstrating how raw text is converted into structured symbols before any meaning is applied.
Parsing
The tokens are then organised into a hierarchical structure known as a parse tree. Given a flat stream of tokens, the parser's job is to group them into nested units that obey the language's grammar.
Without parsing, the compiler cannot answer basic questions like which operations happen first, which values belong together, or what counts as a complete statement. If the tokens do not match the grammar, parsing fails and Python raises a syntax error.
The parser reads tokens from left to right, matches them against grammar rules, and builds a tree when those rules apply. If no rule matches at a given point, parsing stops immediately.
There are many parsing strategies, and the details are a topic of their own. Python uses a parser that closely mirrors the structure of the language's grammar and is easy to reason about.
After parsing, the result is an Abstract Syntax Tree (AST).
The AST represents the structure of the program, not its text. Assignment statements, expressions, and function calls are now explicit nodes in a tree, with clear parent–child relationships.
But structure alone is not enough. The compiler still does not know what any of these names refer to, whether they are allowed in this context, or whether the operations make sense. That is the job of semantic analysis.
Semantic Analysis
Using the AST, the semantic analyser walks the program and applies a set of rules without changing the tree's structure. Instead, it annotates it. Names are linked to definitions, scopes are established, and constraints are attached to nodes.
One of the analyser's responsibilities is resolving names. Every identifier must refer to something that exists and is visible.
c = a + b # error if a or b is not definedEach name in the tree is linked to a definition, whether that comes from the current module, an enclosing scope, or Python's built-in namespace.
Semantic analysis also enforces scope rules.
def f():
x = 5
print(x) # NameErrorPython has function, class, and module scope. Variables defined inside a function do not exist outside of it, and semantic analysis enforces these visibility rules.
Semantic analysis also checks that operations and calls are structurally valid. At this stage, Python knows these represent an addition and a function call, but it may not yet know the concrete types involved. Many checks are therefore deferred until runtime.
a + b
print(c)This is one of the key differences between Python and statically typed languages. Python favours flexibility and performs many semantic checks only when the code is actually executed.
Bytecode Compilation
A traditional compiler like gcc or clang would now turn the AST directly into machine code: binary instructions specific to a particular CPU architecture. An x86 binary will not run on ARM, and vice versa.
Python does something different. Instead of generating machine code, Python compiles the AST into bytecode, a sequence of instructions designed for a virtual machine rather than a physical CPU.
Each AST node is translated into one or more bytecode instructions. For example, an assignment like:
a = 2Becomes two instructions: LOAD_CONST 2, followed by STORE_NAME a. More complex expressions are broken down into similarly small, explicit steps.
The resulting bytecode is portable across platforms with a compatible Python interpreter. Instructions push values onto a stack, operate on them, and store results back into named locations. Because bytecode is not machine code, your CPU cannot execute it directly.
At this point, no code has been executed. The compiler has only produced a sequence of instructions. The values those instructions operate on do not exist yet. That happens next, when the Python Virtual Machine reads the bytecode and executes it instruction by instruction.
This is why many Python errors only appear at runtime. The bytecode is valid and the structure is correct, but whether the actual values support the requested operations is only discovered when the virtual machine runs them.
What begins as plain text ends as a sequence of instructions executed by a virtual machine.