What is Python’s GIL, and when would you reach for multiprocessing instead of threading?
The Global Interpreter Lock is a single lock inside CPython (the standard Python implementation) that only allows one thread to execute Python bytecode at a time, even on a multi-core machine. Threads still work fine for I/O-bound work (network calls, disk reads, waiting on a database) because the GIL is released while a thread is blocked waiting on I/O, letting other threads run during that wait. But for CPU-bound work (heavy computation, image processing, number crunching), threads don’t actually run in parallel, they just take turns on one core, so adding more threads doesn’t speed up CPU-bound Python code at all.
Why multiprocessing is different
multiprocessing sidesteps the GIL entirely by running separate Python processes, each with its own interpreter and its own GIL, so they genuinely run in parallel across CPU cores. The cost is that processes don’t share memory by default, data has to be explicitly serialized (pickled) and passed between them, which adds overhead and complexity compared to threads sharing memory directly.
Best practice
Use threading (or asyncio for a single-threaded event-loop style) for I/O-bound work: web scraping, API calls, database queries, file uploads. Use multiprocessing or a process pool for CPU-bound work: image resizing, scientific computation, data transformation on large datasets. If you need genuine CPU parallelism without the serialization overhead of multiprocessing, consider using a library implemented in C that releases the GIL internally, like NumPy for numerical work, or switching to an alternative Python implementation that doesn’t have a GIL for a specific hot path.
Edge case interviewers probe for
Ask what happens if you mix both: I/O-bound and CPU-bound work in the same threaded program. The CPU-bound thread will hold the GIL for its computation and can starve the I/O-bound threads of their fair share of execution time, since the GIL doesn’t guarantee perfectly even scheduling between threads.
Common mistake
Reaching for threading to “speed up” a CPU-heavy Python function and being confused when it doesn’t get any faster, sometimes even slower due to the overhead of context-switching between threads that are all fighting over the same GIL.
What the interviewer is checking
Whether you understand concurrency versus parallelism as genuinely different things in Python specifically, and can match the right tool (threading, multiprocessing, or asyncio) to the actual bottleneck (I/O wait versus CPU work).
Imagine one chef in a kitchen with only one knife. Threads are like multiple recipes being prepped by that same chef, switching between them. If a recipe involves waiting, like letting dough rise, the chef can put it aside and chop vegetables for a different recipe during the wait, that’s why threads help with waiting-heavy tasks. But if every recipe needs constant knife work with no waiting, having “more recipes going at once” doesn’t help, there’s still only one knife and one chef, so the total chopping speed never actually increases no matter how many recipes you juggle.
Multiprocessing is like hiring more chefs, each with their own knife and their own cutting board, working in completely separate kitchens. Now chopping genuinely happens faster because real parallel knives are moving. The catch is those chefs can’t just hand ingredients to each other instantly since they’re in different kitchens, someone has to physically carry ingredients (serialize data) between them, which takes extra time and effort compared to one chef just reaching across their own counter.
Why interviewers ask this
It’s one of the most common Python-specific gotchas that trips up developers coming from languages without a GIL, and reveals whether you’ve actually hit this wall in practice.
What a strong answer signals
That you distinguish I/O-bound from CPU-bound problems before reaching for a concurrency tool, instead of assuming “threads make things faster” universally.
Common follow-ups
- How does asyncio differ from threading for I/O-bound work?
- Why does NumPy release the GIL for some operations?
- What’s the overhead cost of spinning up a new process versus a thread?
Advanced variation
“Design a system that downloads 10,000 files and then processes each one with heavy computation,” which expects combining both: threads or asyncio for the I/O-bound downloads, then a process pool for the CPU-bound processing step.
A data pipeline needed to download several thousand images from an API and then resize and compress each one. Using a single thread for everything took over 40 minutes, dominated by CPU-bound image processing that never parallelized despite adding more threads. Splitting it into an async download phase (I/O-bound, handled well by asyncio with many concurrent requests) feeding a multiprocessing.Pool for the resize/compress step (CPU-bound, genuinely parallelized across available cores) brought total runtime down to under 10 minutes on an 8-core machine.
from multiprocessing import Pool
from PIL import Image
def resize_and_compress(path):
# CPU-bound work: genuinely parallel across processes, unlike threads.
img = Image.open(path)
img = img.resize((800, 600))
img.save(path, quality=85, optimize=True)
return path
if __name__ == "__main__":
image_paths = [f"downloads/img_{i}.jpg" for i in range(5000)]
# One process per CPU core; each has its own GIL, so this is real parallelism.
with Pool(processes=8) as pool:
pool.map(resize_and_compress, image_paths)- 1The GIL lets only one thread execute Python bytecode at a time within a single process.
- 2Threads still help for I/O-bound work because the GIL releases during I/O waits.
- 3Threads don’t speed up CPU-bound work since only one runs Python code at any instant.
- 4Multiprocessing uses separate processes with their own GIL, achieving real parallelism at the cost of serialization overhead.
- 5Match the tool to the bottleneck: threading or asyncio for I/O-bound, multiprocessing for CPU-bound.