You know V8 cold. Here's CPython's actual pipeline, GC, and threading model — and exactly where your V8 intuition holds and where it breaks.
Lesson 00Time ~16 minType Mental model (knowledge)Prereq None
"Python" the language has several engines. The one you almost always run is CPython — the reference implementation written in C, the analogue of V8 for Node. When you type python (or uv run), you're launching CPython. Everything below is CPython unless noted.
1 · Both are bytecode VMs — but only one JITs by default
Here's the surprise: Python is not "interpreted line by line." Like V8, it compiles your source to bytecode first, then executes that on a virtual machine. The pipelines rhyme:
V8 (Node)
Source.js
Parser→ AST
Ignitionbytecode + interpreter
TurboFan / MaglevJIT: compiles hot functions to machine code
CPython
Source.py
Compilertokenize → AST → bytecode
.pyc cachebytecode saved in __pycache__
ceval loopthe interpreter executes bytecode on a stack machine
The core difference
V8's whole reason for existing is its optimizing JIT: it watches which functions run hot, then recompiles them to specialized machine code (using hidden classes and inline caches). CPython, by default, has no such JIT — the ceval loop interprets the same bytecode every time. That single fact explains most of "why is Python slower than Node for CPU work?" You're comparing an interpreter to a tracing optimizing compiler.
You can see the bytecode — it's not hidden like V8's:
uv run python -c "import dis; dis.dis(lambda a, b: a + b)"# LOAD_FAST a → LOAD_FAST b → BINARY_OP + → RETURN_VALUE
The .pyc files in __pycache__/ (you saw them in the FastAPI project) are just cached bytecode, so re-running a module skips recompilation — the analogue of V8's compilation cache.
2 · CPython has been closing the gap (3.11 → 3.14)
The "no JIT" story is changing, and it's worth knowing because you're on Python 3.14:
3.11 — the specializing adaptive interpreter (PEP 659, "Faster CPython"). The interpreter watches operations at runtime and swaps in specialized bytecodes with inline caches — e.g. an a + b that's always int→int gets a fast int-add op. This is V8's inline-caching idea, brought into the interpreter without a full JIT. Gave ~10–60% speedups.
3.13 — an experimental JIT (PEP 744), using a "copy-and-patch" technique, plus the first free-threaded (no-GIL) build (PEP 703), both opt-in.
3.14 (Oct 2025) — the free-threaded build is now officially supported (PEP 779) though still opt-in; the JIT ships in official macOS/Windows binaries but remains experimental. GIL is still on in the default build.
Takeaway Treat CPython as "a fast interpreter that's growing a JIT," not "a JIT VM like V8." For a backend/API service (I/O-bound), raw CPU speed rarely matters — the network and DB dominate. For CPU-heavy numeric work, people drop into C-backed libraries (numpy, pydantic-core in Rust) rather than relying on the interpreter.
3 · The GIL — the biggest mental-model shift from JS
JavaScript has no threads in the language: one call stack, one event loop, period (Web Workers / worker_threads are fully separate isolates that don't share memory). Python is different: it has real OS threads that do share memory — but a lock called the GIL (Global Interpreter Lock) means only one thread executes Python bytecode at a time.
CPU-bound work + threads
Thread A running bytecode
Thread B waiting on GIL
Thread C waiting on GIL
Threads take turns. No CPU parallelism — 4 cores don't help.
I/O-bound work + threads
Thread A awaiting socket → releases GIL
Thread B running
A thread releases the GIL while blocked on I/O, so others run. Threads DO help here.
Consequences you must internalize:
Threads don't give you CPU parallelism in CPython. For CPU-bound parallel work, use multiprocessing (separate processes, separate GILs) or a C/Rust extension — not threads.
Threads still help for I/O (network, disk), because the GIL is released during blocking calls. This is why FastAPI can safely run your sync def routes in a threadpool (Lesson 17).
async is orthogonal to the GIL.asyncio gives you Node-style single-threaded concurrency on one thread — same event-loop model you know, unrelated to threads or the GIL.
3.14's free-threaded build removes the GIL (opt-in, python3.14t) — real multi-core threads at last, with ~5–10% single-thread overhead. Not the default yet; most of the ecosystem still assumes the GIL.
Why does the GIL even exist? It's the simplest way to make CPython's memory management (next section) thread-safe. Every object's reference count would otherwise need atomic updates on every operation — the GIL made refcounting correct and the interpreter simple. Removing it (PEP 703) required years of careful work for exactly this reason.
4 · Memory: reference counting vs V8's tracing GC
V8 uses a generational, tracing garbage collector (mark-sweep-compact, young/old generations) — it periodically pauses to trace live objects. CPython uses a fundamentally different scheme:
V8
// no per-object counter// GC traces reachable objects// young gen (scavenge) + old gen// stop-the-world pauses, then// mostly incremental/concurrent
CPython
# 1) reference counting (primary)# every object has a refcount# hits 0 → freed IMMEDIATELY# 2) cyclic GC (secondary)# catches reference cycles# refcounting alone can't free
Two-part system:
Reference counting — every object carries a count of references to it. Assigning or passing bumps it; going out of scope drops it. When it hits 0, the object is freed right then — deterministic, no waiting for a GC pass. (This is why with open(...) / __del__ cleanup feels prompt.)
Cyclic garbage collector — refcounting alone can't reclaim cycles (A → B → A never reaches 0). A separate generational collector periodically finds and frees unreachable cycles. You can even see it: import gc.
Practical difference
Refcounting means memory is usually reclaimed the instant the last reference drops (predictable), but it costs a counter update on nearly every operation — part of why pure-Python is slower, and the reason the GIL exists. V8 defers cleanup to GC cycles (less predictable timing, but no per-op counter cost).
5 · "Everything is an object" — literally
In V8, a small integer is a tagged value (SMI) and objects get hidden classes so property access compiles to a fixed offset. In CPython, every value is a heap-allocated PyObject — even 3 is a full object with a type pointer and a refcount. Attribute access typically goes through a dict (__dict__), not a fixed slot.
x = 42id(x) # the object's address (like a pointer)type(x) # <class 'int'> — even ints have a type objectsys.getrefcount(x) # its current reference count
This flexibility (monkey-patching, dynamic attributes, everything introspectable) is exactly what makes attribute access slower than a V8 hidden-class offset — a real trade of speed for dynamism. (3.11's specialization adds inline caches to soften this.)
6 · Many engines, like JS
JS engine
Python equivalent
Note
V8 (Node/Chrome)
CPython
The reference impl — what you run
SpiderMonkey / JSC
PyPy
Alternative impl; PyPy has a real tracing JIT — often much faster
—
MicroPython, GraalPy, Jython
Embedded / JVM / GraalVM targets
🎯 Your win — see the engine
In the REPL, run import dis; dis.dis(lambda a, b: a + b) to read the bytecode your source compiles to. Then run import sys; sys.getrefcount([]) and import gc; gc.get_count(). Seeing bytecode + a live refcount + the cyclic GC counters makes the whole model concrete — that's the win.
Check yourself
Q1. Compared to V8, CPython's default execution model is:
CPython compiles to bytecode then interprets it; there's no optimizing JIT in the default build (3.13+ adds an experimental one).
Q2. The GIL means CPython threads cannot give you:
Only one thread runs bytecode at a time → no CPU parallelism (use multiprocessing). Threads still overlap I/O, since the GIL releases during blocking calls.
Q3. CPython's primary memory-management mechanism is:
Refcounting frees objects the instant the count hits 0; a separate cyclic GC reclaims reference cycles.
💬 Ask your teacher: "when should I actually reach for multiprocessing vs asyncio vs threads?" — the decision table is worth a whole conversation, and it's exactly the kind of thing this model lets you reason about.