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
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:
import dis
def add(a, b):
return a + b
dis.dis(add) RESUME 0
LOAD_FAST_BORROW a
LOAD_FAST_BORROW b
BINARY_OP 0 (+)
RETURN_VALUEThat 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
import py_compile
py_compile.compile("foo.py")
# __pycache__/foo.cpython-314.pycThe 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:
.pycsaves 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:
def f():
retrun 1 # never executed, still failsSyntaxError 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
SyntaxErrorin 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
.pycand when is it regenerated?” - cached bytecode, invalidated by source mtime and size, or by content hash inchecked-hashmode. 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.