There are no Purely Interpreted Languages Anymore
Compiled vs Interpreted Languages
You must have heard phrases like
“Python is slow since it is an interpreted language.”
“C compiles to pure machine code and hence it is much faster.”
But what do they really mean at a fundamental level
Let us take a simple program written in python, called hello.py. You ran the program using the command, python3 hello.py (or maybe you clicked a button in your IDE to run it) and in the terminal appearss, “Hello World”
print("Hello World")
Now, that output was of course expected, but lets think on how and where this program ran. In computers, it is the CPU which does all the processing. But, it only understand instructions in the form of binaries (sequences of 0s and 1s). That is all it understands. So, something translated our code to machine code so that the CPU could understand. The question of when that translation happens and who does it, is the entire compiled-vs-interpreted distinction.
Every program we write has to eventually become machine code. There is no other path. the CPU does not care for our langauges. It says, just give me machine code and I will run it, how you create that machine code (whether via Java, or Python or C or whatever) is your problem.
The interesting question is: when does the translation happen?
You have two options, and they map onto a real-world analogy. Imagine you’re an English speaker who needs to deliver a long speech to a French audience. You can:
- Hand your script to a translator, wait while they produce a full French version, then read the French version aloud.
- Bring a live translator who stands next to you and translates each sentence the moment you say it.
Option 1 is compilation. Option 2 is interpretation. The names aren’t metaphors, they’re the literal English words for these two activities.
The word compile comes from Latin compilare, meaning to gather or heap together. (Think about a compilation of cat videos, a compilation of thriller short stories…). On similar lines, a compiler’s job is to gather up your whole program and produce a translated version of it, a file full of machine code that the CPU can execute directly. That output file is called an executable or a binary.
Here is what the flow looks like

You write code. You run the compiler. You get back a file. You run that file. The compiler is not involved when the program actually executes. Its only job is to compile your program (or your whole codebase)
C, C++, Rust, and Go work this way. When you run gcc hello.c -o hello, you get a file called hello containing machine code. Run ./hello, and the CPU executes those instructions directly. The compiler has long since exited.
An interpreter doesn’t produce a translated file. It reads your source code and executes it directly, statement by statement, while the program is running. The word means exactly what it means in human contexts: explaining the meaning of something, on the spot.

When you run python hello.py, there’s no file called hello left behind. The Python interpreter is itself a program that runs on your machine the entire time your script runs. It reads print("hello"), figures out what to do, and asks the OS to do it. Then it moves to the next line. When your script ends, the interpreter exits.
Python, Ruby, and (traditionally) JavaScript work this way.
Well, Is one better than the other ?
Just like most of the things in Software engineering, here also the thing of concern is trade-offs. Once you see the trade-offs, a lot of language wars will start to make sense.
| Parameter | Compiled (C, Rust, Go) | Interpreted (Python, Ruby) |
|---|---|---|
| Execution speed | Fast — CPU runs native machine code. (Once the code is compiled to its binaries, it is ready to be executed). Analogy → once the translator has translated your whole speech, you are ready to deliver it. The translator’s work is done. | Slower — interpreter is in the loop. It needs to read line by line (send to the OS (i.e. CPU) for execution, get back the result and continue this way). Analogy → the translator is standing next to you during your speech and translating line by line while you delivering the speech. |
| Iteration speed | Slower, must recompile after each change. <brAnalogy → the translator needs to translate the whole speech again. | Faster, just rerun the script. Analogy → the translator is not worried that the script hot got updated. His job is to just read the line of a speech live and translate it then and there. |
| When errors show up | At compile time, before running. Analogy → Imagine your translator finds some mistake in your speech, he will let you know beforehand itself. Hello sir, you need to fix this, we cannot speak this in front of this audience etc | At runtime, when the bad line executes. Analogy → at the time of delivering the speech, some mistake comes up, now the translator is confused what to do, the damage is done infront of the live audience |
| Distribution | Ship the binary Analogy → just take the translated speech text with you, the translator can sleep peacefully in his home | Ship the source + require the interpreter to be present for execution Analogy → take the translator wherever you go since he is needed to stand alongside you during your speech for translation |
| Portability | Binary is tied to OS and CPU. Analogy → Say you are going to give an English speech in Paris, Berlin and Tokyo. Before the tour, you hand your script to a translator who writes out a complete French version. Beautiful. You walk into the Paris venue with the French manuscript and read straight from it. Then you fly to Berlin. The French manuscript is useless. The audience speaks German. You have to start over: find a German translator, produce a fresh German manuscript. Tokyo? Another full translation from scratch. Every new venue requires its own pre-translated script, made specifically for that audience. | Source runs anywhere the interpreter runs Analogy → you carry your original English script everywhere. In Paris you grab a French live translator. In Berlin, a German one. In Tokyo, a Japanese one. Your script itself never changes. As long as each venue has a live translator who speaks English plus the local language, you’re good. |
I gave you the textbook story. The real world is messier, and this is the part that confused me for the longest time. Some of the languages (or rather say interpreters) that follow this strict style are Logo, BASIC and Lisp
Most modern “interpreted” languages don’t actually walk through your source code character by character or line by line. Python first compiles your .py file into bytecode, a compact intermediate format, lower-level than Python but higher-level than machine code. The interpreter then runs the bytecode. Notice the__pycache__ folders in your project, that’s the bytecode. So Python is compiled and interpreted, depending on where you draw the line.
Just in time compilation (JIT), a hack to make interpreted execution faster.
Javascript (JS) uses this. Modern engines like V8 use JIT compilation. The engine starts out interpreting your code, but if it notices a function being called over and over, it stops, compiles that function to machine code on the fly, and uses the compiled version from then on. This is a big part of why JavaScript is fast despite being interpreted.
Java sits squarely in the middle. You compile Java source into bytecode (a .class file) ahead of time, and then the **JVM (**Java Virtual Machine) interprets and JIT-compiles that bytecode at runtime.
How does the interpretaton actually happen (Python example)
Consider a python code
x = 5
y = 10
z = x + y
print(z)
When you type python3 hello.py, the very first thing the shell does is find a program called python3 (a compiled binary, sitting at something like /usr/bin/python3, written in C and compiled to machine code) and start running it. The interpreter is a real running process. Your .py file is just input to that process. Now the process doesn’t start reading the source code line by line and keeps executing it. It follows some stages.
- Read your
.pyfile as text. Literallyopen()and read the bytes. - Tokenize → break the text into chunks: identifier
x, operator=, number5, newline and so on. - Parse → arrange the tokens into a tree that captures the grammar.
- Compile to bytecode → turn the tree into a list of small, low-level instructions. Lower than Python, higher than machine code.
- Execute the bytecode → and this is where the live translation actually happens.
The interpreter contains a big loop, written in C, called the eval loop. Conceptually:
while True:
instruction = next_bytecode_instruction()
if instruction == LOAD_CONST: ...
elif instruction == STORE_NAME: ...
elif instruction == BINARY_OP: ...
... (about a hundred more cases)
Python ships with a module called dis that lets you peek at the bytecode the interpreter will actually walk through. Run this:
import dis
source = """
x = 5
y = 10
z = x + y
print(z)
"""
dis.dis(source)
You get something like this
2 LOAD_CONST 5
STORE_NAME x
3 LOAD_CONST 10
STORE_NAME y
4 LOAD_NAME x
LOAD_NAME y
BINARY_OP +
STORE_NAME z
5 LOAD_NAME print
LOAD_NAME z
CALL 1
When the eval loop sees LOAD_CONST, it runs the C function for LOAD_CONST. When it sees BINARY_OP, it runs the C function for BINARY_OP.
So the truer version of the translator analogy is: you write a script in English. A first translator converts the whole script into shorthand symbols in a private notation only translators know (that’s the bytecode). Then a second translator stands at the podium with the shorthand, reads one symbol, performs the right action, reads the next symbol, performs the next action. That second translator is the eval loop. It exists inside the python3 process and runs the entire time your program runs.
Ok, but then why even call it interpretation → it resembels compilation right
That’s because the second translator never goes away. There’s no standalone executable file for your script. The python3 process is alive throughout, holding your variables, walking the bytecode, executing the C code for each instruction. The instant python3 exits, nothing of your program survives. Compare that to a C binary: you compile once, the compiler exits, and the binary you produced runs on its own forever after, no compiler in sight.
That’s interpretation in the software world. Not “read one line of source, do it”, but “read one bytecode instruction, run the C that implements it, repeat.”
If errors are caught at runtime, how does my IDE catch my mistakes beforehand
So, When you write a python program in IDE and make a mistake, pycharm makes those squiglly little red lines. How does that happen if python is an interpreted language.
Before we look into that, let revisit once again the error showing scenario
In a compiled language, if you misspell a variable name, the compiler refuses to produce a binary. You can’t even run the broken program. In an interpreted language, the misspelled variable is only a problem when execution actually reaches that line, which might be in some rare branch you didn’t test. People have shipped Python code with typos that didn’t crash for months because that branch never ran in production.
The key move is realizing that “checking” and “running” are different things — even for interpreted languages. Look back at those five stages: read, tokenize, parse, compile to bytecode, execute. Only the last one is running your program. Stages 1–4 just inspect the code; they don’t do anything to the outside world.
Quick proof you can try yourself. Make a file broken.py:
print("starting")
x = (
$ python3 broken.py
File "broken.py", line 2
x = (
^
SyntaxError: '(' was never closed
Notice that “starting” never printed. If execution had even begun, you’d have seen it. Python parsed the whole file first, hit the broken parenthesis on line 2, and refused to start executing. So the interpreter itself can find a class of errors without running your code, the parser doesn’t need to run anything to know the grammar is broken.
IDEs just takes this idea further. It ships with its own parser (and a much heavier analyzer on top) and runs it continuously as you type. It’s not executing your program, it’s parsing your file and walking the resulting tree to look for trouble: undefined variables, calling a function with the wrong number of arguments, importing something that doesn’t exist, types that don’t line up. The technical name is static analysis, static because the code is sitting still on disk, not moving (running).
The mental model
Don’t think of “compiled” and “interpreted” as two boxes you put a language into. Think of it as a spectrum based on when translation happens.
- All the way ahead of time → traditional compilation (C, Rust)
- Compile to bytecode ahead of time, finish translation at runtime → Java, C#, modern Python
- Translate during execution, but cache hot paths as machine code (JIT) → JavaScript engines, PyPy
- Translate every line every time it runs → pure interpretation, mostly historical (Logo, Basic etc.)
When someone tells you “language X is compiled” or “language Y is interpreted,” they’re giving you a rough label. The real question, when you actually care about performance, debugging, or deployment, is always: where does translation happen, and what does the user have to ship? That’s the question this whole distinction is trying to answer.