thecodex.expert  ·  The Codex Family of Knowledge
How Languages Work

Interpretation

How an interpreter executes your code without first compiling it to machine code — and why "interpreted" doesn't mean slow.

How Languages Work Requires: Compilation ~20 min read Last verified:
Canonical Definition

Interpretation is the execution of source code or an intermediate representation by a program called an interpreter, which reads and evaluates program instructions at runtime rather than translating the entire program to machine code ahead of time, enabling immediate execution without a separate compilation step and allowing dynamic language features that would be impossible to resolve statically.

One sentence that captures it

An interpreter is a program that reads your code and runs it directly — like a live translator at a meeting — whereas a compiler is like a translator who produces a written translation first and only then hands it over.

The difference from compilation

When you run a Python script, you don't run a separate compiler step first. You just type python script.py and the code runs. The Python interpreter reads your source code and executes it directly. With a compiled language like C, you compile first (gcc program.c -o program), then run the result (./program). Two separate steps.

The interpreter still parses your code — it still builds an AST. But instead of then generating machine code from that AST, it walks the tree and executes each node directly. When it reaches an addition node, it adds two numbers. When it reaches a loop, it loops. No machine code is produced ahead of time.

Why Python compiles too — just not to machine code

Pure tree-walking is slow. Every time a loop runs, the interpreter re-reads and re-evaluates the same AST nodes. Modern interpreters solve this by first compiling source code to bytecode — a compact, lower-level representation that's faster to execute than an AST but still not machine code. The bytecode runs inside a virtual machine (VM).

When you run a Python file, Python first compiles it to bytecode (you might notice .pyc files appearing in a __pycache__ folder — those are the cached bytecode files). The CPython interpreter then executes that bytecode on its virtual machine. This is faster than walking the AST each time, but still slower than native machine code.

Real-world example

The Java Virtual Machine (JVM) takes this further: Java source compiles to bytecode once, and any machine that has a JVM installed can run that bytecode. "Write once, run anywhere" — the JVM is the machine, and it's the same everywhere. This is why Java can run on Windows, macOS, Linux, and Android without recompiling: the bytecode is the same; only the JVM is different per platform.

How Python actually runs

CPython (the reference implementation of Python, written in C) does the following when you run python script.py:

First, it reads the source file and produces tokens. Then it parses those tokens into an AST. Then it compiles that AST to CPython bytecode. Then the CPython virtual machine — a loop that reads one bytecode instruction at a time and executes it — runs the program. Every Python function call, every loop, every arithmetic operation translates to one or more bytecode instructions, each of which the VM handles in sequence.

JIT: the best of both worlds

Modern JavaScript engines — Google's V8 (used in Chrome and Node.js) and Mozilla's SpiderMonkey — use just-in-time (JIT) compilation. The interpreter watches the program as it runs. When it notices that the same code is being executed many times (a "hot" path), it compiles that specific code to machine code on the fly. The next time that code runs, the machine code version executes directly — fast.

This is why JavaScript in a browser can run surprisingly fast despite being a dynamic language. The JIT observes actual runtime behaviour and makes targeted optimisations for the code paths that actually matter.

Why use an interpreter at all?

Interpreted languages offer capabilities that are difficult or impossible to achieve with ahead-of-time compilation. Python can execute code stored in strings (eval()). Ruby can add methods to classes at runtime. JavaScript can load new code over the network and run it. These features require the language machinery to remain present at runtime — which an interpreter provides naturally. A compiled program, once running, doesn't have a compiler available.

Interpreted languages also start faster (no compilation step), are easier to debug interactively, and support REPL environments where you type a line and immediately see the result. These tradeoffs — flexibility and developer experience versus maximum runtime performance — explain why Python is the dominant language for scripting, data science, and rapid development despite being slower than C at raw computation.

Three interpreter architectures

Interpreters exist on a spectrum of implementation complexity, roughly ordered by execution speed:

1. Tree-walking interpreters parse the source into an AST and directly evaluate each node recursively. Simple to implement. Slow for loops (re-evaluates the same nodes). Used in early implementations and educational contexts. Ruby's first implementation (MRI 1.8 and earlier) was tree-walking.

2. Bytecode VMs compile source to a compact instruction set designed for a virtual stack machine or register machine, then execute that bytecode in a dispatch loop. CPython, Ruby's YARV (since 1.9), Lua's VM, and the JVM are bytecode VMs. Faster than tree-walking because the bytecode is already parsed, and dispatch loops are well-optimised by CPU branch predictors.

3. JIT-compiling VMs start as bytecode VMs and selectively compile hot code paths to native machine code during execution. The JVM's HotSpot JIT (Oracle), V8 (Google), SpiderMonkey (Mozilla), and LuaJIT are JIT compilers. Can approach or match ahead-of-time compiled performance for long-running programs.

CPython bytecode in detail

CPython's virtual machine is a stack machine — operations push and pop values from an evaluation stack. The bytecode for x = 1 + 2 is approximately:

CPython bytecode bytecode.txt
LOAD_CONST   1       # push 1 onto the stack
LOAD_CONST   2       # push 2 onto the stack
BINARY_OP    +       # pop two values, add, push result (3)
STORE_NAME   x       # pop result, store as 'x'

You can inspect CPython bytecode directly:

Python inspect_bytecode.py
import dis

def add(x, y):
    return x + y

dis.dis(add)
# Output:
#   RESUME          0
#   LOAD_FAST       0 (x)
#   LOAD_FAST       1 (y)
#   BINARY_OP       0 (+)
#   RETURN_VALUE

JIT compilation: V8 and HotSpot

V8 (Google, open source) uses a tiered compilation strategy. Initially, JavaScript functions are interpreted or compiled by a baseline compiler called Sparkplug — fast compilation, modest optimisation. If a function is called frequently (a "hot" function), the optimising compiler Maglev or Turbofan takes over and produces heavily optimised machine code, using type information gathered from previous executions. If the type assumptions turn out to be wrong (a function that was always called with integers is suddenly called with strings), the optimised code is deoptimised — thrown away — and the function falls back to interpreted execution.

The JVM's HotSpot uses a similar strategy: methods start interpreted, and frequently-called methods are JIT-compiled at increasing optimisation levels (C1 — client, fast compile; C2 — server, heavy optimise). HotSpot also performs aggressive speculative optimisations — for example, inlining a virtual method call if only one implementation has ever been seen. If a second implementation is loaded later, the inlined code is deoptimised.

Interpretation vs. compilation: a formal comparison

Interpretation versus compilation comparison
PropertyAhead-of-time compilationInterpretation / JIT
Startup timeSlow (compilation must complete)Fast (start executing immediately)
Runtime performanceHighest (all optimisations up front)Competitive for hot code; slower for cold
Dynamic featuresLimited (resolved statically)Full (eval, monkey-patching, reflection)
Platform portabilityMust recompile per target ISABytecode runs anywhere with a VM
Memory usageLower (no VM overhead)Higher (VM, JIT code cache)
ExamplesC, C++, Rust, Go, SwiftPython, Ruby, JS, Java, PHP
Commonly confused
Interpreted languages are not inherently slow. The CPython interpreter is slower than compiled C for raw computation. But JavaScript in V8, Java in HotSpot, and LuaJIT are interpreted/JIT-compiled languages that achieve near-native performance in many workloads. "Interpreted" describes the execution model, not the performance ceiling.
Python does compile — just not to machine code. When you run python script.py, Python compiles the source to CPython bytecode before executing. The .pyc files in __pycache__ are this bytecode, cached to avoid recompilation on subsequent runs. Python is not purely "interpreted from source."
JIT compilation is not the same as ahead-of-time (AOT) compilation. AOT compilers translate code before it runs. JIT compilers translate code while it runs, based on observed runtime behaviour. JIT can be more aggressive in some ways (it knows actual types and call frequencies) but cannot perform the lengthy whole-program analysis that AOT compilers can.
How this connects

Threaded code and direct threading

A naive bytecode dispatch loop uses a switch statement over opcode values. Each iteration: read opcode, branch to handler, execute, repeat. The branch predictor typically mispredicts the target (many possible opcodes), causing pipeline stalls. Direct threading replaces the switch with a table of function pointers (or computed gotos in GCC): instead of dispatching on an opcode integer, each bytecode instruction stores the address of its handler directly. At the end of each handler, execution jumps directly to the next handler — no central dispatch loop. CPython 3.11+ uses specialising adaptive interpreter (PEP 659): frequently-executed instructions are replaced in-place with specialised versions optimised for the observed operand types, approaching direct threading in performance.

Inline caching

A key optimisation in dynamic language VMs is inline caching — caching the result of a dynamic lookup (property access, method resolution) at the call site. When obj.method() is executed, the VM must look up method in obj's class hierarchy. Instead of doing this every call, the VM stores the resolved method pointer at the call site and uses it directly on subsequent calls with the same object type. If the type changes, the cache is invalidated. V8 uses polymorphic inline caches (PICs) that can store multiple (type, method) pairs. Hidden classes (V8's internal representation of object shapes) enable property access close to compiled-language speeds.

Garbage collection and the GIL

CPython uses reference counting as its primary garbage collection mechanism — each object maintains a count of references to it; when the count reaches zero, the object is immediately freed. A cyclic garbage collector runs periodically to collect reference cycles (objects that reference each other, keeping both counts above zero despite being unreachable).

CPython's Global Interpreter Lock (GIL) is a mutex that protects CPython's internal state. Only one thread may execute Python bytecode at a time. This simplifies memory management (reference count updates are atomic because only one thread runs) but prevents true parallelism on multi-core hardware for CPU-bound Python threads. The GIL does not affect I/O-bound concurrency (threads release the GIL while waiting for I/O). Python 3.13 introduced an experimental free-threaded build (PEP 703) that removes the GIL at the cost of additional per-object locking.

Specification reference

CPython's bytecode instruction set is documented in the CPython source: Python/ceval.c and Lib/dis.py. The dis module (Python Standard Library documentation at docs.python.org/3/library/dis.html) provides the human-readable interface to CPython bytecode. V8's architecture is documented at v8.dev/docs. The JVM specification is: Lindholm, T., Yellin, F., Bracha, G. & Buckley, A. (2024). The Java Virtual Machine Specification, Java SE 21 Edition. Oracle. Available at docs.oracle.com/javase/specs/.

How this connects
Enables
Key sources
Source confidence: High Last verified: Primary source: Aho et al., Compilers (Dragon Book), 2006

Sources

1
Aho, A. V., Lam, M. S., Sethi, R. & Ullman, J. D. (2006). Compilers: Principles, Techniques, and Tools (2nd ed.). Pearson Education. — Reference for interpreter architectures and VM design.
2
Lindholm, T., Yellin, F., Bracha, G. & Buckley, A. (2024). The Java Virtual Machine Specification, Java SE 21 Edition. Oracle. docs.oracle.com/javase/specs/. — Authoritative specification for JVM bytecode and execution model.
3
Python Software Foundation. dis — Disassembler for Python bytecode. Python Standard Library documentation. docs.python.org/3/library/dis.html. — Authoritative reference for CPython bytecode instructions.
4
Google. V8 JavaScript Engine documentation. v8.dev/docs. — Architecture documentation for V8's JIT compiler pipeline including Sparkplug, Maglev, and Turbofan.
5
PEP 659 — Specialising Adaptive Interpreter. Python Enhancement Proposals. peps.python.org/pep-0659/. — Specification for CPython 3.11's specialising interpreter optimisation.