Compiler vs interpreter
A compiler translates the whole program before it runs and keeps the output; an interpreter translates each line as it reaches it and keeps nothing. The choice moves work between build time and run time, and it moves your errors with it.
A compiler works like a translator who takes the whole book away and hands back a finished translation. An interpreter is the person beside you at the meeting, translating each sentence as it is spoken — nothing is written down for next time.
Your “compiled” language ships with a runtime, and your “interpreted” one compiles at startup. Both facts are true, both sound like contradictions, and neither is — because the split was never between two kinds of language. Both questions come down to the same fact: a compiler and an interpreter both turn source code into something a machine can carry out — they differ in when that translation happens and whether the result gets kept for next time. That one difference in timing is what moves your errors from one place to another.
When the work happens#
A compiler and an interpreter do the same job — turn source code a person wrote into something a machine can carry out — which is why the diagram above draws the same “translate” box on both rows. What differs is when that box runs and what happens to its output afterward.
A compiler walks the entire program once, before execution starts, and keeps whatever it produces: an executable, a set of bytecode, a C file for another compiler to pick up — nothing about being a compiler requires that output to be raw machine code, only that it is produced ahead of time and kept. Every run after the first reuses that same artefact and skips translation entirely, which is the loop arrow labelled “already translated — reuse the artefact” on the diagram’s top row. The same idea shows up outside programming languages entirely: a SQL query planner takes a declarative statement, decides an execution plan before running anything, and can cache that plan for reuse once the statement is prepared — a compiler for SQL, in exactly this before-and-after-execution sense. The plan it settles on can still surprise you, for reasons that have nothing to do with syntax and everything to do with the statistics behind the decision.
An interpreter has no equivalent walk. It reaches a line, translates that line, runs it, and moves to the next one — the diagram draws the translate box again inside each of the three run boxes because that work happens fresh every time a run reaches it, not because three runs were special. A branch nobody takes today is a branch nobody translates today, so whatever is wrong with it stays undiscovered until execution actually walks into it.
None of this is really about types. Whether a mistake is a build failure or a runtime error depends on whether anything reads the whole program before running it — a translation-timing question — and that is a separate axis from whether the language checks types at all. TypeScript’s checker runs ahead of time and then hands off to plain JavaScript, a language with no compile-time type checking of its own; Erlang compiles to bytecode with no static type checking anywhere in the process. A language can be statically typed and interpreted, or dynamically typed and compiled — checking types and translating ahead of time are two passes that often travel together, not two names for the same thing.
Why the line between them is blurry now#
The clean two-row picture above describes two extremes, and real toolchains often sit between them. CPython — the reference implementation most people mean when they say “Python” — takes a .py file, compiles it to bytecode — cached in a pycache file next to the source when that file is imported as a module, thrown away after the run when it is the script you named on the command line — and then runs a virtual machine that interprets the bytecode one instruction at a time. That is compilation followed by interpretation, in one toolchain, on every plain python script.py. Calling Python flatly an interpreted language elides the compile step CPython does before it executes anything; calling it compiled elides that a .pyc file is bytecode for a virtual machine, not something a CPU runs directly. The block-based layout behind Python’s deque is a concrete piece of behaviour that lives inside that same CPython-specific toolchain, not in the language itself, which is exactly why other implementations are free to build it differently.
A JIT complicates the picture from the other direction. Rather than translate everything ahead of time or translate-and-discard on every line, a JIT starts by running unoptimised bytecode and watches which paths actually get taken — which function gets called a thousand times, which loop body never exits early. It then compiles just those hot paths to machine code, mid-run, using information an ahead-of-time compiler never has: the types a value actually turns out to hold, the branch a condition actually takes almost every time. That is why a JIT can end up faster than ahead-of-time compilation on a long-running hot loop — it is not compiling at a handicap, it is compiling with better information, just later.
So “compiled language” and “interpreted language” are nearly always statements about a specific toolchain, not durable facts about a language. The same Python program runs interpreted-over-bytecode under CPython, JIT-compiled under PyPy, and can be turned into a standalone ahead-of-time binary under other tools again — three different points on the same axis, not three different languages.
Picking one on purpose#
The trade is not speed — a compiled program is not automatically the faster one, and a JIT can beat ahead-of-time compilation for exactly the reason above. The real trade is when the cost is paid and when the errors show up. A compile step catches whatever it can see before a single line runs, at the cost of a build you wait through before you can test anything. An interpreter starts running your first line immediately, at the cost of only catching a mistake once execution actually reaches it.
The failure that actually reaches production is treating “it runs on my machine” as proof a program is correct. In a compiled language whose toolchain includes a type checker, a wide category of mistakes cannot survive the build — the compiler reads every line, including the ones your test data never exercises, and rejects the ones that do not check out. An interpreter offers no such guarantee: a typo on a rarely-taken branch — a misspelled variable, a function called with the wrong number of arguments — is not a build failure, because there is no build. It is a runtime error waiting for execution to reach that exact line, and it ships clean, passes review, and passes every test that never walked that path.
That is not an argument against interpreters. It is an argument about test coverage. A compiler’s type checker earns a guarantee for free on every line it can see; an interpreter earns the same guarantee only for the lines something actually ran, whether that something was a test suite or a live request. The branch that fails in production usually was not tested — that gap exists in compiled languages too, since a type checker only proves the shapes line up, not that the logic on that branch is right. What changes is where the gap gets caught: a build catches the shape mistakes before anyone sees them; a runtime error in production is the same class of mistake, caught by the worst possible test.
| Situation | Take | Because |
|---|---|---|
| Errors must surface before shipping | Compiled toolchain | The build is the gate. |
| Fast edit-run loop matters most | Interpreted | No build step between you and the result. |
| Shipping to machines you do not control | Compiled artefact | No runtime to install or match. |
| Long-running hot loops | JIT | It compiles what turns out to be hot. |
IF YOU REMEMBER ONE THING
A compiler and an interpreter run the same translation — one keeps the output and pays for it once, the other pays for it on every run and keeps nothing. Pick by when you want to pay and when you want to find out something is wrong, not by which one sounds faster.
Questions people also ask
5 QUESTIONSIs Python interpreted or compiled?
Neither claim is exactly right on its own — it depends which implementation you mean. CPython, the one almost everyone means by "Python," compiles your source to bytecode first and then interprets that bytecode; PyPy JIT-compiles the hot parts of the same program to machine code at runtime. "Compiled" and "interpreted" describe what an implementation does with the language, not a property of the language itself.
Is compiled always faster?
No. Compiling ahead of time avoids paying a translation cost on every run, but a JIT can still win on a long-running hot loop, because it compiles with information — which branch actually runs, what type a value actually turns out to hold — that an ahead-of-time compiler never has. Faster is a claim about one program's shape and runtime, not a guarantee either approach makes on its own.
What does a JIT actually change?
It moves part of the compile step from before execution to during it, and only for the code that turns out to matter. A JIT starts by interpreting, watches which paths run often, and compiles just those to machine code mid-run — a short delay on code that runs once, in exchange for hot code that ends up running about as fast as anything compiled ahead of time.
Why do type errors appear at different times?
Because whether a type gets checked before your program runs depends on whether anything reads the whole program before running it, and that is a translation-timing question, not a types question. A compiler whose pass includes a type checker can reject an ill-typed line it will never execute; an interpreter with no such upfront pass finds the same mistake only once execution reaches that exact line.
What is bytecode?
An instruction set for a virtual machine rather than for a physical CPU — smaller and simpler to work through than source text, but still one translation step short of what silicon runs directly. CPython's .pyc files hold bytecode; the CPython virtual machine is the interpreter that reads it one instruction at a time.