Spend a little while learning programming and you will meet statements like these:
- C++ is a compiled language, so it is fast.
- Python is an interpreted language, so it is slow.
- Java both compiles and interprets, so it is hybrid.
Those are good enough to survive an introductory lesson. Dig one layer deeper and they start causing trouble. Python also compiles to bytecode, JavaScript engines have JIT compilers, and TypeScript does not even have its own runtime. So what are compiled and interpreted actually describing?
The CPU does not run source code
Take this C++:
std::cout << "Hello, World!\n";
The CPU has no idea what std::cout is. It does not understand if, class, braces or Python indentation either. A CPU executes machine instructions for an architecture such as x86-64 or ARM.
One or more programs therefore have to stand between human-written source and CPU instructions to translate, transform and organise execution. Compilers and interpreters live in this part of the trip.
Source code → compiler → native machine code
↘ interpreter → result
↘ bytecode / IR → VM + interpreter / JIT → result
That diagram is deliberately simple. A real pipeline may add a parser, optimiser, assembler, linker, virtual machine and a small zoo of intermediate representations.
Compilation means translating before execution
With Ahead-of-Time compilation, a compiler processes the program first and produces object code or an executable. When it is time to run, the operating system loads that executable and the CPU runs the machine code already produced.
A typical path looks like this:
source → compiler → object file → linker → executable → CPU
C++ usually takes this route. Go and Rust do too.
One obvious benefit is that syntax and type errors can be stopped during the build. The result is usually specific to an OS and architecture; a Windows x64 build does not magically run on Linux ARM.
Compiled does not automatically mean “fast”. Speed also depends on the algorithm, compiler optimisation, runtime, I/O and the code we wrote. Still, native code prepared in advance often helps startup and execution performance.
Interpretation means a runtime carries out the program
An interpreter receives a program—or an intermediate form of it—and executes it inside a runtime.
“The interpreter reads one source line and runs one source line” is only an introductory picture. CPython, for example, parses and compiles a .py file to Python bytecode first. The Python VM's evaluation loop then handles those bytecode instructions.
An interpreter lets the runtime decide many things while the program is running. The trade-off is another layer between source and CPU whenever the program executes.
Hybrid means mixing stages
Most modern runtimes refuse to sit politely in one box of a table.
Java compiles source to JVM bytecode. A JVM may interpret that bytecode first, then JIT-compile frequently executed methods to native code. A JavaScript engine may also start with an interpreter and optimise hot code through one or more compiler tiers while the program is already running.
So compiled language and interpreted language are labels for the common pipeline of an implementation. A language defines syntax and semantics. Programs such as CPython, V8, a JVM, rustc or the Go compiler decide which route the source actually takes.
Let us walk through the languages one by one.
JavaScript
Running JavaScript with Node.js means handing source to a JavaScript engine, usually V8. The engine parses the code, creates internal representations and begins execution. Frequently run sections may be JIT-compiled into more optimised machine code while the program continues running.
Calling JavaScript an interpreted language is not completely wrong in an introductory class. Saying “JavaScript is only read line by line and is never compiled” is wrong.
console.log in the example is provided by the host environment, such as a browser or Node.js. It is not a JavaScript keyword.
console.log("Hello, World!");When it runs:
JavaScript source → engine parses → bytecode/IR → interpreter/JIT → machine code
The bytecode and compiler tiers are implementation details and may change between engine versions. You do not need to memorise every tier for an interview unless you are applying to the engine team.
TypeScript
TypeScript is easy to misunderstand because it looks like a language with its own runtime. In practice, its types are mainly used for development-time checking and are erased from the JavaScript output.
const message: string = "Hello, World!";
console.log(message);After compilation/transpilation, the important part may be:
const message = "Hello, World!";
console.log(message);
The pipeline:
TypeScript → type-check + emit JavaScript → JavaScript engine → result
The runtime does not know that message once had the type string. If an API returns data of the wrong type, the annotation cannot leap out and save us while the program runs. Runtime data still needs runtime validation.
Python
In the common CPython implementation, a .py file is parsed and compiled to Python bytecode. The Python Virtual Machine executes those bytecode instructions.
message = "Hello, World!"
print(message)Roughly:
Python source → parser/compiler → Python bytecode → CPython evaluation loop → runtime machine code
A .pyc file caches bytecode so a suitable module does not need compiling from scratch each time. It is not a native executable like one produced from C++.
Python has implementations other than CPython, so do not treat CPython details as laws of the whole language. PyPy, for example, has a JIT and therefore a different pipeline.
Java
Java has two visible transformations. javac compiles .java source to JVM bytecode in a .class file. The JVM loads, verifies and executes the bytecode; while running, a JIT compiler may turn hot methods into native code for the current machine.
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}Java source → javac → JVM bytecode → JVM interpreter/JIT → machine code
“Write once, run anywhere” works because many platforms have JVMs that understand the same class-file format, not because every CPU naturally understands Java bytecode. Each platform still needs a suitable JVM.
Go
The Go compiler builds a package and its dependencies into a native executable. Running the binary does not require an interpreter installed to read Go source.
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}Go source → compiler + linker → native executable → CPU
A native executable does not mean “there is no runtime”. A Go binary still contains runtime pieces for garbage collection, goroutine scheduling, stack management and more. Here the runtime is built into the program; it is not a separate interpreter reading source.
Rust
rustc parses source, resolves names, checks types and runs the borrow checker. Invalid ownership or lifetimes stop at compile time. The code then moves through intermediate representations and a backend to produce object code, before linking into an executable. The default backend commonly uses LLVM.
fn main() {
println!("Hello, World!");
}Rust source → rustc checks + optimisation → object code → linker → executable
Rust does not use a garbage collector by default. Its borrow checker verifies many memory-management rules before the program runs. In exchange, when you first learn Rust the compiler gets to scold you with impressive regularity.
C++
C++ has several stages that an IDE often hides, so a beginner sees only a Run button followed by a window appearing.
- The preprocessor handles
#include, macros and conditional compilation. - The compiler turns each translation unit into assembly/object code.
- The assembler produces an object file if the pipeline separates this stage.
- The linker joins object files and libraries into an executable.
#include <iostream>
int main() {
std::cout << "Hello, World!\n";
return 0;
}#include <iostream> does not call a package manager at runtime. The preprocessor brings the necessary declarations into the translation unit; the linker later connects the implementation from the appropriate standard library.
C++ source → preprocess → compile/assemble → object files → link → executable
An undefined reference usually appears during linking: the source had a declaration and therefore compiled, but the linker could not find the definition needed for the executable.
A table worth remembering
| Language | Common route | Where does it finally run? |
|---|---|---|
| JavaScript | Parse → bytecode/IR → interpreter + JIT | JavaScript engine |
| TypeScript | Type-check/emit → JavaScript | JavaScript engine |
| Python | Compile → Python bytecode | Python VM in an implementation such as CPython |
| Java | javac → JVM bytecode → interpreter/JIT | JVM |
| Go | AOT compile + link → executable | CPU with the Go runtime built into the binary |
| Rust | Compile checks + backend + link → executable | CPU |
| C++ | Preprocess + compile + assemble + link | CPU |
If you forget the table, ask three questions:
- What artefact does the source become?
- Is a runtime or virtual machine standing in the middle during execution?
- Is machine code created before execution or while the program runs?
Answering those is more useful than arguing over whether Python “is a compiled language”. Compilation happens, but CPython's common pipeline still needs a VM to execute the bytecode. Labels are shortcuts. The pipeline is what actually happens.