You’ve written a Python file and run it. But what actually happens between pressing Enter and seeing output? This lesson is a short, no-jargon look behind the curtain. You don’t need to memorise any of it — but knowing roughly what’s going on helps when something breaks.
The journey of one line
Take this file:
print("Hello, Python!")
When you run uv run python hello.py, Python does four things, in order:
- Reads the file — opens
hello.pyand reads the text into memory. - Parses the text — checks that the text follows Python’s grammar (no missing colons, no unbalanced brackets). If the grammar is broken, you get a
SyntaxErrorand Python stops here. - Compiles to bytecode — translates your code into a simpler, lower-level form called bytecode. This is what Python actually runs.
- Runs the bytecode — a program called the Python Virtual Machine (PVM) reads the bytecode one instruction at a time and does what each one says.
That’s it. Read → parse → compile → run.
Compiled or interpreted?
People argue about whether Python is “compiled” or “interpreted”. The honest answer: both.
- Python compiles your source code to bytecode (step 3 above).
- Python interprets that bytecode at runtime (step 4).
Compare that with Go or C, where source code is compiled all the way down to machine instructions ahead of time — there’s no interpreter running while the program runs. Python keeps the interpreter alive the whole time, which is one reason Python is slower than Go for raw number-crunching. (It’s also one reason libraries like NumPy exist — they push the slow loops down into C.)
What about .pyc files?
After you’ve run a Python file once, you may notice a __pycache__/ folder appear next to your code:
hello.py
__pycache__/
hello.cpython-313.pyc
The .pyc file is the compiled bytecode Python made when it ran your script. Next time you run the same file, Python checks if the source has changed; if not, it skips the parse-and-compile steps and just runs the cached bytecode. It’s a small speed-up.
You can safely delete __pycache__/ at any time — Python will recreate it. You should never commit it to git.
Errors at different stages
Knowing the four stages helps decode error messages:
SyntaxError— parsing failed. The code isn’t valid Python at all.NameError,TypeError,ZeroDivisionError, … — runtime errors. The code parsed and started running, but something went wrong while the PVM was executing it.
Compare:
print("hello" # missing closing parenthesis
SyntaxError: '(' was never closed
Versus:
print(x) # x doesn't exist
NameError: name 'x' is not defined
The first never started running. The second ran for a moment, then hit a wall.
Why this matters
Two practical takeaways:
- Type hints are not checked at runtime. Python only enforces them through tools like
pyright. If you writedef add(a: int, b: int) -> int: return a + band pass strings, Python will happily concatenate them. The type checker would have caught it. - Print statements run in the order they appear. Sounds obvious, but it’s the foundation of debugging. If a line above your
printblew up, the print never ran. That’s why a sudden lack of output is itself a clue.
What’s next
You know roughly how Python turns text into action. Next, we’ll cover virtual environments — the way to keep each project’s libraries from interfering with every other project on your machine.