Python6 min read

The GIL Is Not a Thread-Safety Contract

What CPython's interpreter lock does, where it is released, and why shared-state invariants still need synchronization.

  • threading
  • GIL
  • concurrency

In the traditional CPython build, the global interpreter lock allows only one thread at a time to execute Python bytecode in a process. That simplifies parts of the interpreter’s memory management, but it does not make an application data-race-free.

The interpreter switches between threads, blocking I/O releases the GIL, and extension modules can release it around native work. Shared state can therefore change between operations that look adjacent in source.

Compound operations contain multiple decisions

if key not in cache:
    cache[key] = compute(key)

Two threads can both observe a missing key, both compute, and both assign. The dictionary remains structurally valid under normal CPython operation, but the application invariant “compute once” has failed.

Protect check-then-act sequences with a lock, use a thread-safe queue to transfer ownership, or tolerate duplicate computation deliberately.

with cache_lock:
    if key not in cache:
        cache[key] = compute(key)

Whether compute belongs inside the lock depends on cost and whether duplicate work is acceptable. A per-key coordination mechanism may reduce contention.

Apparent atomicity is not a language promise

Operations such as list.append execute atomically on every Python implementation because the language reference specifies that built-in container methods cannot be interrupted. It is therefore safe to coordinate producer threads by sharing a list and calling append without a lock.

Even an individually safe mutation does not make a surrounding sequence safe. “Append, then set an event, then update a counter” describes a multi-step state transition visible to other threads.

The GIL can be absent during native work

I/O functions release the interpreter lock while waiting. Performance-oriented extension modules may release it during compression, hashing, numeric kernels, and other operations that do not touch Python objects.

CPU-bound Python threads can therefore never run work in parallel, even when their hot path is inside an extension that explicitly releases the GIL. Multiprocessing is the only route to parallel CPU execution in CPython.

Locks also communicate intent

threading.Lock provides mutual exclusion; RLock allows the owning thread to acquire recursively; conditions combine a lock with state-change notification. Queues often provide the cleanest design because they bundle synchronization with ownership transfer.

Avoid relying on timing or on a tiny thread-switch interval as a test strategy. Stress tests can expose races but cannot prove their absence. Define the invariant, identify every access that participates, and guard it with one documented synchronization policy.

The GIL is an interpreter implementation mechanism. Thread safety is an application property, and the two should not be used as synonyms.