
Post #1 established a genuinely hard constraint: the CPU only executes raw binary machine instructions, via the fetch-decode-execute cycle. Every single line of Python across this entire series — def bubble_sort(items):, class BSTNode:, import hashlib — is readable English-adjacent text, nothing like binary machine instructions at all. This post is the missing piece connecting those two facts: exactly how human-readable source code becomes something a CPU can actually execute, and why Python specifically uses a hybrid approach neither purely compiled nor purely interpreted.
The Pipeline Overview
Source Code (text)
↓
Lexing / Tokenizing
↓
Parsing (builds an Abstract Syntax Tree)
↓
[Compilation to machine code] OR [Interpretation] OR [Bytecode + VM — Python's actual approach]
↓
Execution (Post #1's fetch-decode-execute cycle, eventually)
Every language, regardless of which specific path it takes through this pipeline, starts with the same first two steps.
Lexing: Breaking Text Into Meaningful Pieces
A lexer (or tokenizer) scans raw source code character by character and groups them into meaningful tokens — the smallest units with actual meaning to the language.
source = "x = 5 + 3"
# Conceptually, a lexer produces something like:
tokens = [
("IDENTIFIER", "x"),
("OPERATOR", "="),
("NUMBER", "5"),
("OPERATOR", "+"),
("NUMBER", "3"),
]
This is precisely the first genuine transformation from “an arbitrary string of characters” into “a structured sequence the rest of the pipeline can actually work with.”
Parsing: Building an Abstract Syntax Tree
A parser takes that flat sequence of tokens and builds a tree — and this is worth pausing on directly: it is precisely Post #5’s tree structure, now representing your code’s actual grammatical structure rather than sorted data.
Abstract Syntax Tree for "x = 5 + 3":
Assignment
/ \
x Addition
/ \
5 3
This tree makes the code’s actual structure explicit and unambiguous — critically, it directly encodes operator precedence (2 + 3 * 4 parses so that 3 * 4 forms its own subtree, evaluated before the addition, correctly reflecting that multiplication should happen first) in a way the original flat token sequence did not make explicit at all.
Compilation: Translating Ahead of Time
A compiler takes this tree and translates it, once, ahead of time, directly into machine code — the actual binary instructions Post #1’s CPU can execute directly, with no further translation needed at the moment the program actually runs.
Genuine advantages: the resulting program runs at full native speed, since translation happened entirely beforehand — no translation overhead during actual execution.
Genuine tradeoffs: compilation must happen separately for each target CPU architecture and operating system (a compiled program built for one platform generally will not run directly on another), and the compile step itself takes real time before you can run and test even a small change.
Interpretation: Translating and Executing Together
An interpreter instead reads the AST (or, in simpler interpreters, even the raw source directly) and executes it on the fly, translating and running each part in sequence, with no separate, complete compilation step beforehand.
Genuine advantages: no separate compile step — write code, run it immediately, genuinely useful for fast iteration and interactive experimentation (directly connecting to the REPL environments used throughout this blog’s Python and JavaScript series).
Genuine tradeoffs: generally slower than compiled code, since translation work is repeated every single time the code runs, rather than done once, upfront.
The Middle Ground: Bytecode and a Virtual Machine — Python’s Actual Approach
Python uses neither pure compilation nor pure interpretation — it uses a genuine hybrid, and understanding it precisely resolves something worth being explicit about directly: Python has been called “an interpreted language” throughout casual conversation across this entire blog’s Python content, and that description, while broadly reasonable, is not the complete picture.
import dis
def add(a, b):
return a + b
dis.dis(add)
2 0 LOAD_FAST 0 (a)
2 LOAD_FAST 1 (b)
4 BINARY_ADD
6 RETURN_VALUE
This is Python’s actual bytecode — not machine code Post #1’s CPU can execute directly, and not the original source text either, but a compact, intermediate representation, compiled once from the AST. The Python virtual machine (part of the CPython interpreter itself) then reads and executes this bytecode, one instruction at a time — genuinely faster than repeatedly re-parsing and translating raw source text on every execution, while remaining considerably more portable than directly compiled machine code, since the identical bytecode can run on any platform with a compatible Python virtual machine installed.
JIT Compilation: The Further Middle Ground
This blog’s JavaScript series covered V8’s Just-In-Time (JIT) compilation directly — worth revisiting here with this post’s full vocabulary now established. A JIT compiler starts by interpreting bytecode (exactly as covered above), but actively monitors which specific code paths run repeatedly (“hot” code), and compiles those specific paths directly to genuine machine code partway through execution — getting interpretation’s fast startup and portability for code that runs rarely, combined with compiled code’s genuine execution speed for code that runs constantly. This is precisely why this blog’s JavaScript content described V8’s performance model as “considerably more sophisticated than simple interpretation” — now, with this post’s complete vocabulary, that claim is fully, precisely explained rather than simply asserted.
Compiled vs. Interpreted: The Genuine Tradeoffs, Summarized
| Compiled | Interpreted | Bytecode + VM (Python) | |
|---|---|---|---|
| Execution speed | Fastest | Slowest | Middle ground |
| Startup/iteration speed | Slowest (compile step first) | Fastest | Fast |
| Portability | Tied to target platform | Highly portable | Portable (needs a compatible VM) |
No single approach is universally “better” — each represents a genuine, deliberate engineering tradeoff between execution speed, iteration speed, and portability, and different languages make this tradeoff differently based on their actual, intended use cases.
Real-World Use Cases
Understanding why “compiled languages are always faster” is an oversimplification: JIT compilation, covered directly above, means some “interpreted” languages achieve genuinely competitive performance for frequently-executed code, without sacrificing the fast-iteration benefits of not requiring a separate compile step for every change.
Understanding cross-platform distribution challenges: Directly explains why a compiled program built on one operating system generally cannot run on another without recompilation, while Python’s bytecode-plus-VM approach and JavaScript’s browser-based execution are both inherently more portable across platforms.
Making sense of “why is my first request to this server slow, but subsequent ones fast”: A genuine, common real-world symptom of JIT compilation warming up — early requests run on slower, freshly-interpreted code before the JIT compiler has identified and optimized the actual hot paths.
Choosing a language for a specific project’s actual constraints: Understanding this post’s tradeoffs directly informs genuine, practical language choice decisions — startup-time-sensitive applications favor different tradeoffs than long-running, computation-heavy ones.
Common Mistakes and Gotchas
⚠️ Mistake 1: Assuming “compiled” and “interpreted” are a strict, binary distinction Covered throughout this post — Python’s bytecode-plus-VM hybrid and JIT compilation both demonstrate that real-world languages frequently occupy a genuine middle ground, not one of two mutually exclusive categories.
⚠️ Mistake 2: Assuming interpreted languages are always dramatically slower JIT compilation, covered directly above, has substantially narrowed this gap for many real-world workloads — the actual performance difference depends heavily on the specific language, implementation, and workload, not a fixed, universal rule.
⚠️ Mistake 3: Not understanding why a syntax error is caught before any code runs, even code in a branch that never executes Both parsing (building the AST) and, for bytecode-compiled languages, the bytecode compilation step happen for the entire program before execution begins — a genuine syntax error anywhere in the file is detected during this upfront phase, regardless of whether execution would have ever actually reached that specific line.
⚠️ Mistake 4: Confusing bytecode with actual machine code Covered directly above — Python’s bytecode is not directly executable by Post #1’s CPU; it requires the Python virtual machine to interpret it, a genuinely important, easy-to-miss distinction from a compiled language’s directly-executable machine code output.
Quick Reference
Lexing: source text → tokens
Parsing: tokens → Abstract Syntax Tree (a tree, Post #5)
Compiled: AST → machine code (once, ahead of time) → fast execution
Interpreted: AST → executed directly, translated on the fly, every run
Bytecode + VM: AST → bytecode (once) → VM executes bytecode (Python's approach)
JIT: bytecode → interpreted, with hot paths compiled to machine code live
import dis
dis.dis(your_function) # inspect Python's actual bytecode directly
Exercises
Exercise 1 — Direct application
Using Python’s dis module, inspect the bytecode for a function you wrote earlier in this series (bubble_sort from Post #8 is a good candidate), and identify at least three distinct bytecode instructions.
Exercise 2 — Slight variation
Write out, by hand, what the Abstract Syntax Tree would conceptually look like (using this post’s tree-diagram style) for the expression (2 + 3) * 4, paying specific attention to how the parentheses affect the tree’s structure compared to 2 + 3 * 4.
Exercise 3 — Real-world combination Research whether a language you are curious about (beyond Python and JavaScript, both covered directly in this post) is compiled, interpreted, or uses a hybrid approach, and explain your finding using this post’s vocabulary.
Exercise 4 — Open-ended challenge Explain, in your own words, why Post #1’s fetch-decode-execute cycle is the ultimate, final destination for code from every single approach covered in this post — compiled, interpreted, and bytecode-plus-VM alike — even though they take genuinely different paths to reach it.
FAQ
Q: Is Python “compiled” or “interpreted”? A: Precisely, neither in the pure sense — covered throughout this post, Python compiles source code to bytecode once, then a virtual machine interprets that bytecode; “interpreted language” is a reasonable, common shorthand for this, but this post’s full explanation is the accurate, complete picture.
Q: Why don’t all languages just use JIT compilation, if it offers the best of both approaches? A: JIT compilation adds genuine implementation complexity and its own runtime overhead (monitoring which code paths are “hot,” and performing the live compilation itself) — for genuinely short-running scripts, this overhead may never be recovered before the program finishes, which is one real reason simpler interpretation or straightforward ahead-of-time compilation remain the better fit for many use cases.
Q: Does the AST covered in this post relate to Post #5 and Post #10’s tree and recursion content directly? A: Yes, directly and concretely — an AST is precisely a tree (Post #5), and both parsing an AST into existence and later processing it (whether compiling or interpreting) are naturally, commonly implemented using the recursive techniques covered in Post #10, since a nested expression’s structure is itself naturally self-similar.
Q: Can I see the actual machine code a compiled program produces?
A: Yes, using specialized tools (disassemblers) that translate raw machine code back into human-readable assembly language — genuinely beyond this introductory post’s scope, but a natural, deeper extension of the dis.dis() bytecode inspection this post covered directly for Python specifically.
Summary and Next Steps
You now understand the complete journey from Python source code (or any language’s source code) to actual CPU execution — lexing into tokens, parsing into an Abstract Syntax Tree (precisely Post #5’s tree structure, applied to code itself), and then either direct compilation, direct interpretation, or Python’s actual bytecode-plus-VM hybrid approach, with JIT compilation as a further, sophisticated middle ground this blog’s JavaScript content referenced without full explanation until now. Post #1’s fetch-decode-execute cycle, established at this series’ very start, is the final, common destination every single one of these paths eventually reaches.
Your next step: Complete Exercise 1 — inspecting real Python bytecode for a function you’ve already written — since seeing your own familiar code’s actual intermediate representation directly is what makes this post’s entire pipeline concrete rather than abstract description.
Last updated: August 2026.



