Running python script.py first goes through normal shell PATH resolution to find the python executable — this is why an inactive virtualenv silently uses the wrong interpreter.
The interpreter process launches, initializes the core runtime (memory allocator, built-in types, sys module), and sets up sys.path — the list of directories Python will search for imports.
script.py's source text is tokenized (keywords, identifiers, operators) and parsed into an Abstract Syntax Tree — a structural representation of the code, independent of formatting or comments.
ParseThe AST is compiled into Python bytecode — a lower-level, portable instruction set (not machine code). This is the same bytecode format used whether the script runs on Linux, macOS, or Windows.
CompileUp to this point it's pure translation: text → tokens → AST → bytecode. No line of your code's logic has executed. Execution only begins in the next step, inside the Python Virtual Machine.
CheckpointWhen execution hits an import requests line, Python checks sys.modules (already-imported cache) first, then searches sys.path in order — this is exactly why the wrong virtualenv silently imports the wrong (or a missing) package.
For imported modules (not the entry script itself), CPython writes compiled bytecode to __pycache__/*.pyc, keyed by source hash/timestamp — so re-running later skips steps 3-4 for unchanged modules, speeding up subsequent runs.
CacheThe PVM is a stack-based interpreter loop — it reads bytecode instructions one at a time (LOAD_NAME, CALL_FUNCTION, etc.) and executes them against a stack of Python objects. This loop is where your actual program logic runs.
ExecuteIn standard CPython, the Global Interpreter Lock ensures only one thread executes Python bytecode at a time, even on a multi-core machine. Threads can still overlap for I/O-bound work, but pure CPU-bound Python code doesn't parallelize across threads because of this.
GILEvery function call pushes a new frame onto the interpreter's call stack, holding local variables and the current bytecode position. This is what a Python traceback is literally printing when a script errors out.
FramesWhen the script finishes (or raises an uncaught exception), the process exits with a code: 0 for clean completion, non-zero on an unhandled exception — this exit code is exactly what a shell script or CI pipeline checks to decide success/failure.
Exit| Term | Meaning |
|---|---|
| CPython | The standard, reference implementation of Python most people mean by "Python" |
| AST | Abstract Syntax Tree — structural representation of parsed source code |
| Bytecode | Portable, low-level instructions compiled from the AST, run by the PVM |
| PVM | Python Virtual Machine — the stack-based loop that executes bytecode |
| sys.path | Ordered list of directories searched when resolving an import |
| sys.modules | Cache of already-imported modules, checked before re-importing |
| GIL | Global Interpreter Lock — serializes bytecode execution to one thread at a time in CPython |
| __pycache__ | Directory holding cached .pyc bytecode for imported modules |
| Frame | Per-call-stack record of local variables and bytecode position |
| Exit Code | Integer the process returns to the OS/shell on completion — 0 means success |