AlgoMaster Logo

Global Interpreter Lock (GIL)

High Priority13 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

The GIL is the single most misunderstood thing about Python concurrency, and you can't make sensible choices between threads and processes without understanding it. This lesson explains what the GIL actually is, why CPython has one, when it gets released, and the concrete consequences for code that mixes CPU-heavy work with threads.

What the GIL Is

The Global Interpreter Lock is a mutex inside the CPython interpreter that guarantees only one thread can execute Python bytecode at a time. Even on a machine with 16 cores, a pure-Python program with 16 threads will have exactly one of them running Python code at any given moment. The other 15 are waiting for the GIL.

That's it. That's the whole definition. The complications come from what "Python bytecode" includes and what counts as "running" it.

A few clarifications up front:

  • The GIL belongs to CPython, the reference implementation you almost certainly use. Jython and IronPython have no GIL. PyPy has one. The free-threaded build of Python 3.13 is starting to remove it, more on that later.
  • The GIL is per interpreter, not per process. Each subinterpreter (a newer feature) has its own GIL, which is part of why subinterpreters are interesting.
  • The GIL protects the interpreter's internal state, not your data. Your shared list is still going to need its own lock when multiple threads mutate it.

The sys._is_gil_enabled() function was added in Python 3.13 for the experimental free-threaded build. On any normal CPython install, the GIL is on and there's no way to turn it off at the language level.

Why the GIL Exists

The GIL is a design decision that goes back to the early 1990s, and it pays for three things CPython gets in return.

Reference counting safety. CPython uses reference counting to manage memory. Every Python object has a counter that tracks how many names point to it. When the counter hits zero, the object is freed. If two threads incremented or decremented the counter at the same time on different cores, the counter would corrupt and objects would leak or get freed too early. The GIL makes every Py_INCREF and Py_DECREF automatically thread-safe because only one thread can run interpreter code at a time.

Simpler C extensions. Thousands of third-party C extensions (NumPy, lxml, Pillow, database drivers) were written assuming the GIL exists. They use Python's C API freely, modify reference counts, and touch interpreter state without locking anything themselves. Removing the GIL means every one of those extensions has to be audited and probably rewritten. The GIL is the contract that lets the C ecosystem stay simple.

Single-threaded performance. Fine-grained locking inside the interpreter would slow down single-threaded code, which is what almost all Python programs actually do. Past attempts to remove the GIL produced an interpreter that ran threaded code faster but single-threaded code 30 to 50% slower. That trade was unacceptable for the language as a whole.

The trade-off is brutal but clear: CPython chose simplicity, C compatibility, and single-threaded speed over true thread-level parallelism. For most Python use cases (scripts, web servers handling I/O, data pipelines built on NumPy) the trade pays off. For pure-Python CPU-heavy work, it hurts.

Threads Still Don't Run Python in Parallel

The clearest way to see the GIL is to run the same CPU-heavy work sequentially, then with two threads, then with two processes. The threaded version should not get faster. The process version should.

The CPU-heavy task here is computing a "recommendation score" for many products by repeatedly multiplying and summing floats. It's deliberately pure Python so the GIL applies the whole time.

Two threads, two cores, same work, no speedup. The threaded version is even slightly slower because of GIL contention overhead. The two threads spend their time taking turns holding the GIL, not running in parallel.

Now the same workload with processes instead:

Roughly half the time. Each process has its own Python interpreter, its own GIL, and runs on a separate core in genuine parallel. The threaded version was capped at one core's worth of Python, the multiprocess version was not.

The same pattern holds whether the work is matrix math written in Python, parsing JSON with the standard library, or compressing data with a pure-Python implementation. Anything that's "Python running bytecode on a CPU" gets serialized by the GIL.

When the GIL Is Released

The GIL is not a single uninterruptible lock for the lifetime of a thread. It gets released and reacquired constantly. There are three main moments to know about.

During I/O. When a thread calls into the operating system to read a file, send a network request, wait on a socket, or sleep, the C code that performs the syscall releases the GIL before the call and reacquires it after. The OS does the waiting, the GIL stays free for other threads to grab. This is the entire reason threading works well for I/O-bound code.

Around long-running C extensions. Library code written in C can voluntarily release the GIL when it does work that doesn't touch Python objects. NumPy releases the GIL around array operations on numeric data. lxml releases it around parsing. The standard hashlib releases it around large hash computations. The C extension promises "I won't touch Python state for a while", drops the GIL, does its work, and reacquires the lock.

Every "check interval". Even when a thread is in pure Python bytecode, it doesn't hold the GIL forever. Periodically (every 100 bytecodes in older Python, or every 5 milliseconds in 3.2+) the running thread releases the GIL to give other threads a chance. You can read and tune this with sys.getswitchinterval() and sys.setswitchinterval().

That 5 ms interval is why a pure-Python loop on one thread doesn't completely starve the others. Every 5 ms or so, the running thread surrenders the GIL, the OS picks a waiting thread, that thread acquires the GIL and runs for a while, and so on.

The I/O case is where threads actually help. Consider fetching product details from a slow source. Each fetch sits waiting on a response, doing nothing on the CPU. While one thread waits, the GIL is free, so another thread can run.

Four fetches, each takes one second, but the threaded version finishes in one second total. While each thread is parked inside time.sleep (a syscall, GIL released), the other three threads can do their own waiting. The GIL is not the bottleneck because nobody is running Python bytecode.

This is the rule of thumb: threads help when threads spend their time waiting, not when they spend their time computing.

Visualizing GIL Contention

Picture the GIL as a single microphone in a room of four people. They can all be present, but only one of them can be speaking into the microphone at a time. The others wait for the microphone, take their turn, and pass it back.

The diagram below shows four threads on a four-core machine, all wanting to run pure Python code. Only one holds the GIL at any moment. The others are runnable but parked, waiting for the lock.

Now compare that to threads doing I/O. Three of them are parked inside recv() or read(), the GIL is free, and the fourth holds it while it does whatever small amount of Python work it has to do.

The first picture is the CPU-bound case: four threads, four cores, one runs at a time. The second picture is the I/O-bound case: four threads, four cores, three are waiting on the network, the fourth runs unimpeded. Same number of threads, completely different parallelism story, and the difference is entirely about whether the work releases the GIL or not.

C Extensions That Release the GIL

A subset of CPU-heavy work in Python does scale with threads, and it's the subset that runs inside a C extension that has chosen to release the GIL during its compute phase. The most common examples are NumPy, SciPy, pandas (for some operations), and image libraries like Pillow.

A NumPy element-wise multiply on a 10-million-element array spends almost all its time inside a C loop that has dropped the GIL. While one thread is inside that loop, another thread can hold the GIL and start its own NumPy call, which immediately drops the GIL again. Both threads end up doing real work on different cores at the same time.

That's roughly a 1.8x speedup on two threads, close to what you'd hope for. The pure-Python loop earlier saw zero speedup on the same hardware. The only difference is whether the CPU-heavy work is happening inside Python bytecode (GIL held) or inside a C extension that has released the GIL.

This is why threading is not a dead concept for CPU work. It's just that the work has to live in a library that releases the GIL. The list of major libraries that do is short but covers a lot of real-world numeric and image work.

A simple mental model: if the inner loop of your hot path is in for x in some_list: ... in Python, threading won't help. If the inner loop is in numpy.dot(a, b) or Image.filter(...), threading might.

What This Means for Choosing Threads vs Processes

The GIL doesn't kill concurrency in Python, it just shapes the choice. Most decisions reduce to a few rules of thumb that follow directly from what you've seen above.

Use threads when work is dominated by waiting. Network calls, file I/O, database queries, slow APIs. Threads spend almost all their time outside the GIL, so multiple of them can wait in parallel. A web server fetching data from 10 backends benefits enormously from this.

Use processes when work is dominated by Python bytecode. Pure-Python data transformations, parsing, simulations, anything where your hot loop is for x in items: ... compute ... in Python. Each process gets its own interpreter and its own GIL, so they run on different cores in genuine parallel.

Threads can also work for CPU-bound code in C extensions that release the GIL. NumPy, SciPy, Pillow, lxml, and similar libraries are the main examples. If your hot path is mostly inside one of these, threads can give real speedup. Don't assume it, measure it.

Use asyncio when you have thousands of concurrent waits. Threads have OS-level overhead per thread (memory for stacks, scheduling cost). Asyncio handles thousands of concurrent I/O operations on a single thread without that overhead.

The table below summarizes the choices, GIL-flavored.

WorkloadGIL impactThreadsProcessesasyncio
Fetching from many slow APIsGIL released during I/OGoodOverkillBest at scale
Pure-Python CPU loopsFully GIL-boundNo speedupBig speedupNo speedup
NumPy / SciPy / image computeGIL released in CReal speedupReal speedupNo (sync work)
Mix of small CPU and I/OPartialModest gainModest gainBest
Many short tasks (microseconds each)Overhead dominatesUse a poolAvoidBest

The takeaway is not "threads are useless" or "always use processes". It's that the GIL pushes you toward different concurrency models for different workloads, and the wrong model gives zero speedup or worse.

Free-Threaded Python (Experimental)

The story above describes every CPython release up to and including 3.12. Python 3.13 introduced an experimental free-threaded build, often called "no-GIL" Python, based on PEP 703. In a free-threaded build, the global lock is removed and CPython uses fine-grained locking and biased reference counting to keep the interpreter safe across threads.

In the free-threaded build, the CPU-bound threading example from earlier would actually scale. Two threads doing pure-Python work on two cores would take roughly half the time of one. The trade is real: single-threaded code in 3.13 free-threaded is currently 5 to 10% slower than the regular build, and some C extensions don't work yet because they assume the GIL. The PEP plans for a multi-year transition where the free-threaded build is opt-in, then default-but-opt-out, then standard.

For now, treat free-threaded Python as a research preview. Production code should still assume the GIL exists. The patterns in the rest of this section, including processes for CPU work, hold regardless of whether the free-threaded build eventually replaces the regular one.

Quiz

Global Interpreter Lock (GIL) Quiz

10 quizzes