A computer's processor only understands machine code, so every program written in Python, C# or Go has to be translated before it can run. A compiler and an interpreter are two different answers to 'when does that translation happen?', and the answer shapes how fast your program runs, when you find your mistakes and how you ship your code.
The same job, two timetables
Both tools take source code, the text you write, and turn it into something the machine can act on. Neither is better in general. They make different trade-offs about when the work is done.
- A compiler translates the whole program first, producing a new file (an executable, or some intermediate form). Running the program is a separate step that can happen later, as many times as you like.
- An interpreter reads your program and carries out each instruction as it goes. There is no finished translation left behind: the next time you run the program, the interpreter reads it again.
The rest of the differences follow from that one choice.
How a compiler works
A compiler reads all of your source code before anything runs. Roughly, it:
- Splits the text into tokens: keywords, names, numbers, symbols.
- Parses those tokens into a tree that captures the program's structure.
- Checks the tree for errors it can prove, such as a call to a function that doesn't exist or, in a statically typed language, a string where a number should be.
- Optimises the code, for example by working out constant expressions once or removing code that can never run.
- Writes out the result, often machine code for one kind of processor and operating system.
The output is a standalone program. Your users run that file, not your source code, and they don't need the compiler installed. The expensive part, the translation and optimisation, is paid once at build time. Every run after that starts from ready-made machine code, which is why compiled programs are usually fast.
The cost is the wait. You change one line, rebuild, then run. On a large project a build can take long enough to break your concentration, and a mistake in your logic only shows up after that build has finished and the program runs.
How an interpreter works
An interpreter skips the separate build. You hand it your source file and it starts executing: read an instruction, do it, move to the next one.
That gives you instant feedback. Open a Python prompt, type 2 + 2, and the answer appears straight away. It's why interpreted languages dominate scripting, data exploration in notebooks and quick experiments: the gap between 'I wonder if this works' and 'now I know' is tiny.
The cost is repeated work. Every time the program runs, the interpreter has to work out what each instruction means all over again. A loop that runs a million times gets decoded, in some form, a million times. For a short script that overhead doesn't matter. For a tight numeric loop it can make an interpreted version many times slower than a compiled one.
One bug, two ways
Here is the same small mistake, adding a number to a piece of text, in Python and in C#.
print("Starting the order")
total = "3" + 4
print("Total:", total)Run it and Python prints Starting the order, then stops on the second line with a TypeError: it can't join a string and a number. The first line did its work before the problem was found, because Python only checks that operation when it reaches it.
Console.WriteLine("Starting the order");
int total = "3" + 4;
Console.WriteLine($"Total: {total}");The C# compiler refuses to build this at all. It reports error CS0029, that it can't convert a string to an int, and no program is produced, so nothing runs, not even the first line.
Neither behaviour is wrong. Python let you run the part that worked and showed you exactly where it failed. C# stopped you before any user could hit the bug, at the price of a build step. If a line like that sat inside a rarely used branch, the Python version could pass casual testing and fail months later, which is why tests matter so much in interpreted code. For more on dealing with failures once they happen, see error handling.
Where the line blurs: bytecode and JIT
Very few popular languages today are purely one or the other. Most use a mix.
Bytecode. Python first compiles each file into bytecode, a compact set of instructions for a virtual machine rather than for a real processor. You may have seen the .pyc files it caches in __pycache__ folders. CPython, the standard Python, then interprets that bytecode. You can look at it yourself:
import dis
def add(a, b):
return a + b
dis.dis(add)This prints instructions such as LOAD_FAST, the steps the Python virtual machine carries out. The exact names vary between Python versions.
Just-in-time (JIT) compilation. Java and C# compile to bytecode too, and their runtimes turn it into machine code while the program is running. Java's standard runtime, like the JavaScript engines in browsers, starts by interpreting, watches which parts run most often, and compiles those 'hot' parts into optimised machine code. Here is the path code takes through a runtime like that:
A JIT gets some of both worlds: the program starts quickly without a long up-front build, and the code that matters most ends up running as machine code. The trade-off is a slower warm-up and a runtime that uses more memory.
This is why 'compiled language' and 'interpreted language' really describe how a language is usually run, not something fixed in the language. PyPy, for example, is a Python implementation with a JIT compiler.
When each fits
Reach for an ahead-of-time compiled language, such as C, C++, Go or Rust, when:
- speed or predictable performance matters, as in games, databases or command-line tools;
- you want to ship a single file that runs without installing a language runtime;
- you want as many mistakes as possible caught before the code reaches anyone.
Reach for an interpreted or bytecode language, such as Python, Ruby or JavaScript, when:
- you're scripting, automating a task or exploring data, and fast feedback matters more than raw speed;
- the same code should run anywhere the interpreter is installed, without a build per platform;
- the slow parts of the job are the network or the disk, not your own loops.
Plenty of real systems mix the two: a Python program calling libraries written in C, for instance, so the heavy number crunching runs as compiled code.
Common mistakes
- Thinking 'interpreted' means 'no checks at all'. Python still rejects a syntax error before it runs a single line, because the whole file is compiled to bytecode first.
- Mixing up 'compiled' with 'statically typed'. The C# example failed to build because C# checks types before running, not simply because it's compiled. Plenty of compiled code still fails at run time.
- Believing that if it compiles, it works. A compiler catches what it can prove from the code, such as syntax and types. It can't tell that you meant to multiply rather than add.
- Judging speed by the label alone. A JIT-compiled program can come close to compiled speed once it has warmed up, and a well-written script that spends its time waiting on a database won't get much faster by being rewritten in a compiled language.
Key takeaways
- A compiler translates the whole program first, then runs it quickly as often as you like.
- An interpreter carries out code as it reads it: instant feedback, but the reading is repeated on every run.
- Compilers can catch some errors before anything runs; interpreters surface them when the line is reached.
- Most modern languages mix both, using bytecode and JIT compilation.
- Choose by what matters most for the job: run speed and early checks, or fast feedback and portability.