CPython’s Global Interpreter Lock
The Global Interpreter Lock (GIL) is a lock in the standard build of CPython—the main implementation of Python—that allows only one thread at a time to execute Python bytecode within an interpreter. It protects CPython’s internal data structures, not your application’s business rules or every sequence of operations in your code.
That distinction explains a common surprise: Python threads can overlap while waiting for I/O, but multiple threads usually do not make pure Python computation run in parallel on multiple CPU cores.
The problem the GIL solves
Python programs constantly manipulate shared interpreter state. Every Python object has bookkeeping attached to it, including a reference count, which tracks how many active references point to that object. Objects also contain implementation details such as their type, contents, and links to other objects.
Without coordination, two operating-system threads could update the same reference count simultaneously. One update could overwrite the other, leaving CPython with the wrong count. An object might then be freed while another thread still uses it, or never freed at all. More complex operations could observe an object halfway through an internal change.
CPython could protect each object or internal structure with many separate locks. That would allow more parallelism, but it would also make the interpreter substantially more complicated. Code would need to acquire locks in a safe order, avoid deadlocks, and pay locking costs throughout ordinary single-threaded execution.
The GIL is a simpler design: a thread must hold one interpreter-wide lock while it runs Python bytecode and accesses most of CPython’s internal state. This makes the interpreter’s own bookkeeping manageable and keeps the single-threaded case relatively efficient.
The GIL is primarily an implementation lock. It is not a general-purpose lock for making your program’s shared data logically safe.
How threads contend for it
A Python threading.Thread is backed by an operating-system thread, so several such threads can be runnable at once. But in a normal GIL-enabled CPython build, only the thread holding the GIL can execute Python bytecode.
A simplified sequence looks like this:
- A thread obtains the GIL and runs Python code.
- Other runnable Python threads reach the point where they need the GIL and wait.
- CPython periodically gives the running thread a chance to release the GIL.
- Another waiting thread obtains it and continues running.
The relevant setting, sys.setswitchinterval(), controls the interpreter’s preferred interval for switching between threads. It is a scheduling hint, not a promise that a thread will run for exactly that amount of time. The operating system, long-running native code, and extension behavior also affect when another thread gets to run.
The handoff does not make the threads execute Python simultaneously. It makes them take turns, often rapidly enough that an I/O-heavy program appears to make progress on many tasks at once.
Why I/O-bound threads work well
I/O-bound work spends significant time waiting for something outside the CPU: a network response, a file operation, a database result, or a timer. While a thread is blocked in many such operations, CPython releases the GIL so another thread can run Python code.
For example, these requests can overlap their waiting time:
import threading
import urllib.request
def fetch(url):
with urllib.request.urlopen(url) as response:
response.read()
threads = [
threading.Thread(target=fetch, args=(url,))
for url in ["https://example.test/a", "https://example.test/b"]
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
While one thread waits for bytes from the network, another can run, start its own request, or process a completed response. The total elapsed time can therefore be much lower than performing the requests one after another. The threads are not making Python instructions run in parallel; they are using waiting time productively.
This release behavior is implemented by CPython and by many libraries through native code. A C extension can explicitly release the GIL around a blocking operation, then reacquire it before it touches Python objects again. A library that fails to release it during a long blocking operation can prevent unrelated Python threads from running efficiently.
Why CPU-bound threads usually do not scale
CPU-bound work spends most of its time executing instructions rather than waiting. Consider two threads calculating expensive results in pure Python. Each thread repeatedly needs the GIL, so one runs for a while, yields, and the other runs for a while. Across both threads, there is still generally only one thread executing Python bytecode at any instant.
import threading
def count_work():
total = 0
for number in range(20_000_000):
total += number
return total
threads = [threading.Thread(target=count_work) for _ in range(2)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
This can be slower than one thread because switching between contenders has overhead, and the threads compete for the same lock. It does not turn two CPU cores into two independent Python-bytecode executors.
For CPU-heavy parallelism, a program commonly uses multiple processes. Each process has its own interpreter and its own GIL, allowing different cores to execute at the same time, though exchanging data between processes costs more than sharing memory between threads. Some native libraries also release the GIL while performing substantial computation, allowing their own native threads to run in parallel; that is a property of the library’s implementation, not of arbitrary Python code.
What the GIL does not guarantee
The GIL does not make a sequence of application operations atomic. For example, “check whether a value is present, then update it” is a multi-step decision that can interleave with another thread. Protect such invariants with threading.Lock, a queue, or another explicit coordination mechanism.
The GIL is also not part of Python’s language definition. It describes the usual CPython build. Recent CPython releases include experimental or optional free-threaded builds that remove this restriction, and their compatibility and performance trade-offs differ. When an incident report says threads are “blocked on the GIL,” however, it normally means the standard CPython runtime: one thread is executing Python code while other runnable threads are waiting to take their turn.