Compilers vs interpreters vs "kind of both"
Why some languages translate everything upfront, others read and run line by line, and why most real languages today are secretly a mix of the two.
The recipe book vs the live translator
Imagine two different ways to cook a French recipe if you only speak English.
Option A: Hire a translator who reads the entire recipe first, writes out a complete English version, hands it to you, and leaves. From then on you just cook from your English copy — the translator never needs to be in the kitchen again.
Option B: Hire a translator who stands next to you the whole time, reading one line of the French recipe, telling you what it means in English, waiting for you to do that step, then moving to the next line — over and over, for the entire recipe, every single time you cook it.
Option A is a compiler: it translates the whole program upfront into something that can run independently (machine code), and then gets out of the way. Option B is an interpreter: it reads and executes your program's instructions one at a time, every single time you run it, translating on the fly.
Compiling: translate once, run many times
source code → [ compiler ] → machine code file → run it whenever, no translator needed
(main.cpp) (main.exe)
g++ main.cpp -o main finishes, the file main is a complete, standalone set of machine instructions. You can delete main.cpp, unplug from the internet, hand main to a friend with the same kind of processor, and it will still run — the compiler already finished its job and isn't needed anymore. This is why compiled languages like C++ tend to run very fast: all the hard thinking happened once, ahead of time (this is called ahead-of-time, or AOT, compilation).
The tradeoff: you must fully re-run the compiler after every change, and the resulting file only works on processors compatible with the machine code it contains.
Interpreting: translate and run together, every time
source code → [ interpreter reads line 1 ] → runs it
→ [ interpreter reads line 2 ] → runs it
→ ... repeats for every line, every single run
.exe file produced. Every time you type python script.py, the interpreter opens script.py fresh and works through it line by line, translating and immediately executing as it goes. This makes changes instant to test (no separate build step) but slower to run overall, since translation work gets redone every single time, even for a script you've run a thousand times before.
Why "purely compiled" and "purely interpreted" are rare in practice
Here's the part almost every course skips, and it matters a lot in real systems: very few modern languages actually sit at either extreme. Most combine both ideas, because they want an interpreter's fast startup/testing loop and a compiler's raw execution speed.
Bytecode + virtual machine: Java and (in its real implementation) Python don't compile directly to a specific processor's machine code, and they don't purely interpret raw source text either. Instead, a compiler-like stage first translates your source into an intermediate, simplified instruction set called bytecode — not tied to any specific real processor. A separate program called a virtual machine (VM) then interprets that bytecode. This is a genuinely important idea you'll build a small version of yourself in this course's final module.
Java: .java source → [javac compiler] → .class bytecode → [JVM interprets/JITs] → runs
Python: .py source → [compiled to] → .pyc bytecode → [CPython VM interprets] → runs
Just-in-time (JIT) compilation: Many bytecode VMs (the JVM, modern JavaScript engines like V8) get clever: they interpret bytecode at first, but watch which parts of the program run over and over (called "hot" code), and compile just those parts into real machine code while the program is still running, swapping in the faster version transparently. This is why a long-running JavaScript loop in your browser often speeds up the longer it runs — the JIT compiler is quietly upgrading it in the background.
Comparing the trade-offs
| Compiled (AOT) | Interpreted | Bytecode + JIT | |
|---|---|---|---|
| Translation happens | Once, before running | Every single run | Once to bytecode, then adaptively to machine code |
| Startup speed | Instant (already translated) | Instant (no separate build step) | Fast startup, speeds up over time |
| Peak execution speed | Fastest | Slowest | Close to compiled, after warmup |
| Portability | Tied to target processor | Very portable (same source runs anywhere the interpreter exists) | Portable bytecode, fast on any machine with a VM |
| Example | C, C++, Rust | Shell scripts, basic Python execution model | Java (JVM), JavaScript (V8), C# (.NET CLR) |
A little math: why "translate once" wins for long-running programs
Suppose a program's core logic runs inside a loop executed times, and translating one line of source takes time while running one already-translated instruction takes time (with , since running a switch is far cheaper than figuring out what it should be).
A pure interpreter re-translates every line on every one of the iterations:
A compiler pays the translation cost exactly once, up front, regardless of how many times the loop runs:
For small these look similar, but as grows large (a loop that runs millions of times, which is extremely common), the compiled version's cost is dominated entirely by , while the interpreted version's cost grows by the much larger term. This single piece of arithmetic is the entire economic argument for why performance-critical software (game engines, databases, this very website's server) is written in compiled or JIT-compiled languages rather than purely interpreted ones.
Common mistakes
- Thinking "interpreted language" and "compiled language" are properties of the language itself. They're properties of a specific implementation. Python has interpreter implementations (CPython) but also compiler-like ones (Cython, Nuitka). C has interpreters too, though they're rare in practice.
- Assuming compiled always beats interpreted for every task. For a script you run once and throw away, an interpreter's zero-build-step convenience often wins outright — the "translate once" advantage only pays off when a program runs long enough or often enough to matter.
- Confusing bytecode with machine code. Bytecode is still an abstraction — a simplified, portable instruction set invented for a specific VM. It is not something your physical processor can execute directly.
Summary
A compiler translates an entire program upfront into something that can run on its own; an interpreter translates and executes line by line, every single run, trading raw speed for zero build step and easy portability. Most real-world languages today use a hybrid — compiling to portable bytecode, then interpreting or JIT-compiling that bytecode — to get a workable balance of fast startup and fast execution. This course focuses on the "translate ahead of time" compiler path in full detail, but the phases you'll learn (lexing, parsing, code generation) are exactly the same phases bytecode compilers and JIT compilers use internally.