← the series

Is Python compiled? Yes. Is it interpreted? Also yes.

Ask any tutorial and you'll get the confident answer: Python is an interpreted language. Now ask Python itself. It ships with a module called dis, short for disassembler, and if you hand it one of your functions, it prints out the bytecode that Python compiled your function into. Read that sentence again. There is a compiler inside Python. It runs every single time you run a script. The "interpreted language" has been compiling your code all along, and it will happily show you the output.

Actually, you've probably already seen the receipts without knowing it. That __pycache__ folder that keeps appearing in your projects, full of .pyc files you may have deleted or gitignored a hundred times? Those are compiled Python files. Cached bytecode, saved to disk so the compiler can skip redoing work next run. You have been shipping around compiler output your entire Python life.

The honest CPython pipeline

So here's what actually happens when you run python app.py, and by now, four parts into this series, every step will look familiar. Your source gets lexed into tokens and parsed into a tree. The tree gets compiled, genuinely compiled, that word means the same thing it meant in part 3, into bytecode: instructions for an imaginary machine, exactly like part 4 described. Then a virtual machine inside Python walks that bytecode, one instruction at a time.

Compare that with Java and the difference turns out to be almost embarrassingly small. Java compiles to bytecode in a separate visible step and saves the result; Python does it invisibly at startup and caches it in __pycache__. Java's VM eventually JIT-compiles the hot paths; the standard Python VM historically just kept interpreting. That's it. That's the mighty categorical wall between "compiled Java" and "interpreted Python": a build step you can see versus one you can't, and a JIT versus none. Not two species. Two configurations of the same pipeline.

The distinction the tutorials skip

Here's the idea that finally dissolves the question, and honestly the single most useful thing I learned in this whole rabbit hole. Python the language and the program that runs your Python are two different things. The language is a set of rules about what programs mean: what for does, how dictionaries behave, when exceptions fire. The thing you installed is CPython, a specific program, written mostly in C, that enforces those rules. It's called the reference implementation, because it's the one the language's own developers maintain. But it is an implementation. One of several.

PyPy runs the same language with a completely different strategy: it watches your program run and JIT-compiles the hot paths to machine code, part 4 style, which makes long-running Python often dramatically faster. People have built Pythons that run on the JVM. There are Pythons for embedded chips. Same language, same rules, wildly different execution strategies underneath.

Python the language is a set of rules at the top. Below it, implementations: CPython compiles to bytecode and interprets it, PyPy adds a JIT compiler, and other implementations exist. Compiled or interpreted is a property of the bottom boxes, not the top one. Python · the language a set of rules about what programs mean CPython the default one. compiles to bytecode, interprets it on a VM PyPy same language, plus a JIT. often much faster on long runs others Pythons for the JVM, for microcontrollers, for research "is Python compiled?" points at the top box. compiling is something the bottom boxes do.
Languages are specs. Implementations have strategies. The old question aimed at the wrong layer.

Once you see this split, the old question falls apart on contact. "Is Python compiled?" has no answer at the language level, because compiling isn't something a language is, it's something a tool does. Even the supposedly clean cases wobble: nothing about C forbids interpreting it, and C interpreters exist, mostly as curiosities, because nothing about "C-ness" lives in the compilation step. The property we were taught as identity was always just the default toolchain's strategy.

Everyone is secretly everything

Now do the audit on the other "interpreted" celebrity. JavaScript runs on engines like V8, the one inside Chrome and Node. V8 parses your JS into a tree, compiles the tree to bytecode, and starts interpreting, watching for hot code exactly like the JVM does. Hot functions get JIT-compiled to machine code mid-run. (Modern V8 actually has extra compiler tiers in between, but that's the shape.) So the language your tutorial filed under "interpreted" is running through two compilers on a busy afternoon.

Java, the "awkward middle child" of the taxonomy, turns out to have been the honest one all along: compiled to bytecode before the run, interpreted at the start of the run, JIT-compiled during it. All three, openly, and nobody could figure out which bucket it belonged in because the buckets were the problem. Even CPython is drifting: recent versions have been shipping an experimental JIT. The flagship "interpreted language" is growing a compiler that runs at runtime, and nobody's identity is threatened, because these were never identities. They're strategies, and strategies get mixed.

An audit grid. For Java, JavaScript on V8, and Python on CPython, the answers to compiled, interpreted, and JIT compiled are yes almost everywhere. CPython's JIT is marked experimental. Java JavaScript (V8) Python (CPython) compiled? yes to bytecode yes to bytecode yes to bytecode interpreted? yes yes yes JIT compiled? yes yes experimental in recent versions the two-bucket taxonomy, audited. every "interpreted" language here contains a compiler.
Three "different kinds" of languages giving nearly identical answers. The buckets weren't describing the languages. They were describing defaults.

What the labels are still good for

I don't want to leave you thinking the words are useless, because they do carry real information, just about something smaller than we were told. "Python is interpreted" is a decent compressed way of saying: the default workflow has no separate build step, errors surface while the program runs, startup is instant, and top speed is capped by interpreter overhead. "Rust is compiled" compresses: there's a build wait, errors surface up front, you ship a standalone binary, and it runs at native speed. Those are genuinely different developer experiences and genuinely different performance profiles. They're just properties of toolchain defaults, not of some essence baked into the language. Change the tool, and the "identity" changes with it. PyPy users are writing an interpreted language with a JIT. Nobody's Python stopped being Python.

One loose end

There's exactly one thread left hanging, and it's been dangling since part 1. Whatever the strategy, ahead of time, interpreted, JIT, everything eventually bottoms out as machine code executing on one particular CPU. And machine code has a property this whole series has been dancing around: it doesn't travel. A binary built on my machine can be complete gibberish on yours, and the reasons why explain everything from "download for Mac or Windows?" to why Java's weird bytecode detour conquered the enterprise. That's the finale: why your program won't run on my machine.

← back to the series