Backend / Python core / 01_interpreted_vs_compiled.md

Is Python interpreted or compiled?

Updated 5 interview angles 3 min read source
On this page5
  1. The two stages
  2. Where the bytecode is cached
  3. Why “interpreted” is the wrong word alone
  4. What that costs at runtime
  5. Interview angle

Is Python interpreted or compiled?

Both, and the interviewer is checking whether you know there is a compilation step at all. Source is compiled to bytecode, and a virtual machine interprets the bytecode.

The two stages

text
foo.py  ──compile──▶  bytecode  ──interpret──▶  effects
                      (cached in __pycache__)   (CPython VM)

The first stage happens once per file version and is invisible. The second happens on every execution and is where the time goes.

You can watch the first stage directly:

python
import dis

def add(a, b):
    return a + b

dis.dis(add)
text
  RESUME                   0
  LOAD_FAST_BORROW         a
  LOAD_FAST_BORROW         b
  BINARY_OP                0 (+)
  RETURN_VALUE

That is the compiled artefact. BINARY_OP is one instruction, and at runtime it still has to ask what a and b are before it can add them — which is the whole answer to “why is it slower than C”.

Where the bytecode is cached

python
import py_compile

py_compile.compile("foo.py")
# __pycache__/foo.cpython-314.pyc

The interpreter tag in the filename is why a .pyc from another version is ignored rather than mis-executed. Invalidation is by source mtime and size by default, or by content hash with --invalidation-mode checked-hash — which is what you want in a container where mtime is not stable.

Gotcha: .pyc saves compilation time on import, not execution time. A service that starts slowly benefits; a service with a hot loop does not. Shipping precompiled bytecode is a startup optimisation.

Why “interpreted” is the wrong word alone

Claim Reality
Runs line by line compiles the whole module first
No compile step there is one, it is cached
No compile errors a SyntaxError is a compile error

The third row is the demonstration. This fails before a single line runs:

python
def f():
    retrun 1        # never executed, still fails

SyntaxError at import time. If Python truly executed line by line, the error would surface only when f was called.

What that costs at runtime

No ahead-of-time type information, so every operation dispatches on the runtime type; every integer is a heap object rather than a machine word. As of 2026, CPython 3.13+ ships an experimental JIT that specialises hot bytecode, which narrows the gap on some workloads and does not close it.

The practical consequence, and the useful thing to say: the fix for a slow Python hot path is not a faster interpreter, it is doing the work somewhere else — a C extension, NumPy, or a different process. See Profiling Python — cProfile, pstats, timeit.

Interview angle 5

  • “Is Python interpreted or compiled?” - both. Source compiles to bytecode, cached in __pycache__, and the CPython VM interprets that. Calling it “just interpreted” misses the step the question is actually about.
  • “Prove there’s a compile step.” - a SyntaxError in a function that is never called still stops the program at import. The whole module was compiled before anything ran.
  • “Why is it slower than C?” - dynamic typing means each operation dispatches on runtime type, and values are boxed objects rather than machine words. There is no ahead-of-time optimisation, though 3.13+ has an experimental JIT for hot paths.
  • “What is .pyc and when is it regenerated?” - cached bytecode, invalidated by source mtime and size, or by content hash in checked-hash mode. It saves import time, not execution time, so it helps startup rather than throughput.
  • “How would you speed up a hot loop?” - move the work out of the interpreter: a C extension, a vectorised NumPy operation, or another process. Micro-optimising Python-level code rarely reaches the same order of gain.