CPython’s Global Interpreter Lock
CPython’s global interpreter lock (GIL) is a mutex that allows only one operating-system thread at a time to execute Python bytecode in a traditional CPython process. It protects the interpreter’s internal state, not every piece of data your program can access.
That distinction explains both the GIL’s limits and why native extensions can release it.
The problem the GIL solves
CPython represents nearly everything in a Python program—integers, strings, lists, functions and modules—as heap objects managed by the interpreter. These objects carry bookkeeping such as a reference count, the number of active references to an object.
When a reference is created or removed, CPython updates that count. When the count reaches zero, it may immediately destroy the object. Many other interpreter operations also update shared structures: importing a module, allocating memory, changing a type’s dictionary, or running the garbage collector.
Without coordination, two threads could update the same reference count concurrently and lose one of the updates. Worse, one thread could destroy an object while another still believed it was valid. Internal hash tables, allocator state and interpreter bookkeeping would need fine-grained locks throughout the runtime.
The GIL is CPython’s broad coordination mechanism. It makes those internal operations occur under one runtime-wide lock, allowing much of CPython’s object model and C extension interface to be implemented without a lock around every individual object operation.
The GIL is primarily an implementation lock for CPython. It is not a general-purpose lock for your application’s shared variables.
What happens when a Python thread runs
Suppose two Python threads are ready to execute ordinary Python code. Each has an operating-system thread and a CPython thread state, a record containing information such as the thread’s current execution frame, exception state and interpreter association.
Before entering the bytecode evaluation loop, a thread must hold the GIL and have its thread state attached as the current state for that interpreter. The evaluation loop then repeatedly fetches and executes Python bytecode while holding the lock.
Only one thread can be in that part of the interpreter at once. The other thread can exist, be scheduled by the operating system and wait for the GIL, but it cannot simultaneously execute Python bytecode in that interpreter.
CPython periodically gives waiting threads a chance to acquire the lock. The exact scheduling details have changed across Python versions, and the interpreter’s switch interval is a scheduling hint rather than a promise that a thread runs for an exact number of milliseconds. A thread may also give up the GIL when it performs an operation that waits, such as blocking input/output.
When the first thread releases the GIL, it must detach or save the relevant thread state before another thread can run Python code. When it later resumes, it reacquires the GIL and restores that state. This pairing ensures that CPython knows which interpreter and execution context are active on the current operating-system thread.
The result is concurrency without parallel Python execution: multiple Python threads can make progress over time, but CPU-bound Python bytecode generally does not run on multiple cores at the same instant. This is the source of the familiar complaint that adding threads does not speed up a pure-Python computation.
What the GIL does and does not protect
The GIL protects assumptions made by CPython’s runtime while the lock is held. It helps make reference counting, object allocation, type operations and access to interpreter-owned structures safe from simultaneous execution by other Python threads.
It does not automatically make a sequence of application operations atomic. For example, this is a read, computation and write, not one indivisible action:
counter = counter + 1
Another thread can run between those steps. Use a threading.Lock or another appropriate synchronization primitive when multiple application operations must act as one transaction.
Nor does the GIL protect arbitrary native memory. A C or Rust extension’s own data structures still need their own locks or other synchronization when accessed by multiple threads. Even operations that often appear atomic in a particular CPython version should not be treated as a substitute for an explicit application-level synchronization design.
Why extensions release it
A native extension can spend time doing work that does not need the Python interpreter. Two common examples are waiting for a file, socket or database response, and performing a long calculation over native data such as an image buffer.
Holding the GIL during that work would block every other Python thread in the interpreter, even though the extension is not examining Python objects. A well-behaved extension therefore follows this pattern:
- Save the current thread state.
- Release the GIL.
- Perform blocking or independent native work.
- Reacquire the GIL.
- Restore the thread state and continue interacting with Python.
CPython’s C API provides macros commonly used for this pattern, including Py_BEGIN_ALLOW_THREADS and Py_END_ALLOW_THREADS. The exact implementation saves and restores thread state around the unlocked section; it is not merely a flag saying that the thread is busy.
While the GIL is released, another thread can execute Python bytecode. If the native work is CPU-heavy and genuinely independent, multiple extension calls can run in parallel on different cores. If it is blocking I/O, other Python threads can continue instead of waiting behind the blocked call.
The restriction is important: code in the unlocked section must not call ordinary Python C API functions or touch Python objects in ways that require the interpreter lock. The extension must keep its inputs in safe native form, use its own synchronization where necessary, and reacquire the GIL before creating objects, modifying Python-visible state, raising an exception or calling back into Python.
This is why a Python program can benefit from threads even though pure Python CPU work usually cannot. The GIL serializes execution that needs CPython’s interpreter, while native extensions can temporarily step outside that protected region for work that is independent or waiting.