Can an Interpreted Language Really Do Multithreading?
Python, the GIL, and the myths that surround it
The Setup: It works, but it's "fake"?
I built an async worker. Its job was to pull data from a bunch of network devices, and running it sequentially was painfully slow — so I parallelized it with threads. The difference was immediate. Several times faster. Feeling pretty good about myself, I mentioned it to a colleague, and here's what came back:
"Python multithreading is fake. Because of the GIL, it all runs on a single core and just pretends to be parallel. If you want real parallelism, you need something like Celery."
I was thrown. But it clearly did get faster? Was the speedup I saw an illusion? I couldn't just let that itch go unscratched.
The Mechanism: What exactly does the GIL lock?
The GIL (Global Interpreter Lock) is a single lock inside the Python interpreter. At any given moment, only one thread can execute Python bytecode. If you stop there, my colleague is right — no matter how many threads you spawn, execution gets lined up single file.
But the crucial part comes next. The GIL is released during I/O waits.
Picture a restaurant with exactly one cook
Imagine a kitchen with a single cook. This cook can only do one motion at a time. That's the GIL: one cook in the kitchen, and that cook can't perform two actions simultaneously.
Now the cook puts a stew on the stove. It needs to simmer for 10 minutes. Does the cook just stand there staring at the pot? Of course not. During those 10 minutes, they crack eggs and make an omelette on the side. Halfway through the omelette they start boiling pasta, and while the pasta cooks, they go back to season the stew.
Here, "time on the stove," "time the pasta boils" — that's the I/O wait. During that time, the cook (Python) doesn't sit idle; they move on to another dish. One cook, yet multiple dishes progress as if in parallel. That's what happens when the GIL is released during I/O.
My worker was doing exactly this. When I send "give me your data" to device A, there's a network round-trip before the response arrives (= the stew simmering). During that wait, the thread fires a request to device B, and while waiting on B, sends one to device C. The waits overlap, and total time collapses. The speedup I saw was no illusion.
We live like this every day:
- Washing machine: You don't stare at it for 30 minutes. You vacuum while it runs.
- Instant noodles: While the water boils, you chop scallions and crack an egg.
- Coffee: While the drip brews, you toast some bread.
Any task with "waiting time" lets you slot another task into that gap. Python threads do precisely this.
The Turn: When it actually is fake
So was my colleague wrong? No. There's a clear case where he's right: CPU-bound work.
Now you have to dice 100 onions by hand
Back to the kitchen. This time the job is dicing 100 onions by hand. There's no "waiting time" here. The moment the cook stops chopping, the work stops. There's no "set it and wait" phase like the stew.
Handing over 100 order tickets (threads) changes nothing. There's one cook, and that one cook has to keep chopping. Worse, if they bounce between "a bit of this order, a bit of that one," they only lose time moving cutting boards and switching hands (context switching). One pair of hands, but more juggling.
That's CPU-bound. Image processing, heavy math, bulk data transformation in pure Python — these keep Python's hands (the CPU) busy, so the GIL never gets a chance to release. Here, my colleague is right: multithreading is "fake."
So how do you dice the onions faster?
You hire more cooks. Give four cooks their own board and knife each, and now four people really do dice onions at the same time. Four boards, four knives — no waiting on each other.
That's Celery (or multiprocessing). It doesn't add threads (order tickets); it adds cooks (processes). Each process gets its own GIL. Multiple locks (boards) mean Python code genuinely runs on multiple cores at once. Communication between cooks is brokered by something like Redis or RabbitMQ (the kitchen manager).
Wait — what about splitting into stations (grill, sauté, fry)?
While writing this, I considered another analogy: a restaurant with separate stations — grill, sauté, fry — where orders come in centrally and get routed to each station, and each station cooks independently. Isn't that the GIL?
But thinking it through, that's closer to Celery than to GIL threads. "Each station cooking simultaneously and independently" means there are multiple cooks. If each station has its own person with their own stove and board, that's multiprocessing.
A thread under the GIL is always a single cook. That one cook simply reuses the idle time (simmering, boiling, waiting) so cleverly that it looks like several people are working. That subtle difference — "are there really many people, or is one person just using the gaps well?" — is the heart of understanding the GIL.
Here's the summary:
| I/O-bound work | CPU-bound work | Complexity | |
|---|---|---|---|
| Multithreading (1 cook) | ✅ Effective (reuses wait time) | ❌ No benefit | Low |
| Celery (many cooks) | ✅ Effective | ✅ Effective | High (needs a broker) |
The Lesson: Ask "why," don't accept the verdict
The lesson here wasn't really the technical knowledge. "Multithreading is fake because of the GIL" is only half true. And drop the condition from that half, and it becomes fully false.
Had I taken my colleague's word for it, I'd have wasted time rewriting working code into Celery. Had I dismissed it ("but it got faster!"), I'd have made the same mistake of throwing threads at CPU-bound work.
And the question in the title — "Can an interpreted language really do multithreading?" — becomes clear here too. The GIL is not the fate of interpreted languages; it's a choice made by CPython, one specific implementation. The same Python has no GIL on Jython (runs on the JVM) or IronPython, and starting with CPython 3.13 there's an experimental free-threaded build that turns the GIL off entirely. So the accurate sentence isn't "interpreted languages can't parallelize" but "the CPython I happen to use chose an approach called the GIL." If you can't separate the essence of a language from the choices of its implementation, you'll mistake one implementation's limit for the whole language's limit.
There's only one way to stay steady when you hear a technical verdict: dig into the "why" all the way down. Then you can see the conditions under which the claim holds and the ones under which it breaks — and figure out which situation you're actually in. In the end it wasn't a question of right or wrong, but of "is my work I/O-bound or CPU-bound?" and "which implementation am I running?"
Look at the premise a conclusion stands on, not the conclusion itself. That's what I took away.