The engineering behind free-threaded Python.
For about 30 years, Python had a simple rule: only one thread could run Python code at a time, no matter how many CPU cores you had.
That rule is now optional.
Taking away the GIL sounds easy until you hit the real question:
How do you keep millions of tiny memory touches safe without slowing Python down?
That's what this article is about.
The one-knife kitchen
Picture a kitchen with eight chefs and one knife.
Whoever has the knife cooks. Everyone else waits.
That's the GIL — the Global Interpreter Lock. With one active thread at a time, Python's internals (refcounts, dicts, type data) stayed safe without extra work.
Simple. But your 8-core machine behaved like a single core.
The real question when you remove the GIL
Now any thread can run anytime. Two threads can hit the same memory at once.
CPython touches memory constantly — attribute lookups, method calls, passing variables around.
The obvious fix? Lock every object.
That works on paper. In practice it's terrible.
A real Python program does attribute lookups and refcount updates constantly. Put a lock on each of those and everything slows down — even code that only uses one thread.
So the CPython team didn't pick one strategy for everything:
Match the tool to how often the work happens and how much it touches.
The engineering toolbox — from cheapest to heaviest
Free-threaded CPython uses four techniques. Here's each one, from lightest to heaviest.
1. No lock — the hottest paths
- Cost: basically free
- Safety: none by itself — must be paired with atomics
- Used for: reads that almost never change, like the pointer to an object's
__dict__during attribute lookup
Attribute lookups happen billions of times. A lock on each one would kill performance. So on the hottest paths, CPython just reads the value. No lock.
The catch: whoever writes that value has to publish it safely — usually with atomics.
2. Atomics — single-value updates
- Cost: nearly free — one CPU instruction
- Safety: the value is read or written in one indivisible step
- Used for: reference counters, pointer stores, small flags
Think of it like swapping a mailbox label in one motion. You see the old label or the new one — never a torn mix of both.
The reader takes no lock. The writer uses an atomic store. Both stay cheap. Neither blocks the other.
Why not atomics for everything? They only protect one value at a time. If you need to update three related fields and keep them consistent, atomics can't help.
3. Per-object locks — multi-step changes
- Cost: moderate — real locking, but only on that one object
- Safety: full — nobody else touches this object while you're working on it
- Used for: operations that change several fields, or call into code that might do anything
When a change is too messy for a single atomic step, CPython locks just that one object. Two threads working on two different lists don't block each other.
Locks cost more than atomics. But they're the right tool when you need several safe steps in a row.
4. Stop-the-world — rare global work
- Cost: very high — every thread pauses briefly
- Safety: total — nothing else in the program is running
- Used for: the garbage collector, which needs a snapshot of every object to find unreachable cycles
The GC only runs occasionally, and its work is inherently global. Fine-grained locks everywhere would be a nightmare. Lock-free is basically impossible here.
So Python pauses all threads briefly, runs the GC, and resumes. Heavy when it happens — but rare enough that the average cost stays small.
The design principle behind all of it
Look at those four tools together and one idea shows up:
The more often something runs, the cheaper its safety has to be.
- Attribute reads (billions per second) → no lock
- Refcount updates (millions per second) → atomics
- Object mutations (thousands per second) → per-object lock
- Garbage collection (a few times per minute) → stop the world
Not "no locks ever." Not "locks on everything."
Just: use the lightest thing that still works.
That's why single-threaded free-threaded Python only pays a small overhead — hot paths got atomics, not heavier locks. And multi-threaded code can finally scale because the big locks aren't global anymore.
One more decision — and this one wasn't about code
Fixing CPython's own internals is only half the problem.
Python has thousands of C extensions — NumPy, Pillow, database drivers, ML wheels. Most were written assuming the GIL would always be there.
Nobody can audit all of them. And turning off the GIL for everyone on day one would've broken a lot of stuff quietly.
So they solved it on the policy side. Three roles:
- Library author — tests their C extension and sets a flag if it's safe without the GIL
- Python — checks that flag at import. Flag present → keep going. No flag → turn the GIL back on for the whole process and warn you
- You — don't configure anything. It just happens
The trade-off:
- Cost: import one extension that hasn't declared itself safe, and you lose parallelism for that entire run
- Benefit: old libraries keep working. No crashes. No corrupted data. No mysterious hangs
They could've shipped sooner and hoped for the best. Or waited years until every library was ready. This middle path lets the ecosystem move one library at a time.
Same idea as the runtime tools — put the decision with whoever actually knows the code:
Match the responsibility to whoever has the knowledge.
What this means for you as a Python developer
You don't need to memorize any of this to use free-threaded Python.
Two things worth keeping in mind:
1. The GIL never protected your application logic.
If two threads wrote to the same dict, you needed your own lock before. You still do. The GIL only protected Python's internals.
2. Check your dependencies.
One unmarked C extension can put the GIL back for your whole program. Whether you actually get parallel threads depends on your libraries, not just your Python build.
The takeaway
Four tools. One rule: use the smallest safety step that still works.
- No lock where reads are hottest
- Atomics for single updates
- Per-object locks for big changes
- Short pause when the GC needs the full picture
- Library authors say what's safe — Python plays safe when they don't
That's not removing the GIL. That's rebuilding around it.
Officially supported since Python 3.14, but still opt-in. Default Python hasn't changed.
The knife is optional now.
Eight chefs can cook at once — but only because Python figured out who holds which tool, and when.