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:

  1. Reads the file — opens hello.py and reads the text into memory.
  2. 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 SyntaxError and Python stops here.
  3. Compiles to bytecode — translates your code into a simpler, lower-level form called bytecode. This is what Python actually runs.
  4. 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:

  1. Type hints are not checked at runtime. Python only enforces them through tools like pyright. If you write def add(a: int, b: int) -> int: return a + b and pass strings, Python will happily concatenate them. The type checker would have caught it.
  2. Print statements run in the order they appear. Sounds obvious, but it’s the foundation of debugging. If a line above your print blew 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.

Toggle theme (T)