Python
GIL
Global Interpreter Lock
programming
concurrency

Why the Global Interpreter Lock?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The Global Interpreter Lock, or GIL, is one of the main reasons Python threading behaves differently from threading in many other languages. It exists in CPython to protect the interpreter's internal state, especially around object management, and the tradeoff is that CPU-bound Python threads do not run Python bytecode truly in parallel.

What the GIL Actually Does

In CPython, the GIL is a process-wide lock that allows only one thread at a time to execute Python bytecode. Multiple threads can exist, block on I/O, sleep, and wait, but when it comes to executing Python instructions inside the interpreter, only one thread holds the lock at a given moment.

That is why people say "Python threads do not run in parallel." More precisely, Python threads in CPython do not execute Python bytecode in parallel. That distinction matters, because I/O-bound threading can still be very useful.

Why CPython Has the GIL

The short answer is implementation simplicity and performance tradeoffs.

CPython has historically relied heavily on reference counting for memory management. Every Python object tracks how many references point to it. When the count reaches zero, the object can be freed. Without a lock, those reference count updates would need much more synchronization, which would complicate the interpreter and add overhead to many common operations.

The GIL makes object model invariants easier to maintain and keeps the single-threaded case relatively fast. That was a very practical tradeoff for a language whose popularity grew around ease of use, extension modules, and predictable C API behavior.

Why It Hurts CPU-Bound Threads

If two Python threads both spend their time doing CPU work, they compete for the GIL instead of using two cores efficiently. A simple example demonstrates the idea:

python
1import threading
2
3def count():
4    total = 0
5    for i in range(10_000_000):
6        total += i
7    return total
8
9threads = [threading.Thread(target=count) for _ in range(2)]
10
11for thread in threads:
12    thread.start()
13
14for thread in threads:
15    thread.join()

This uses two threads, but in CPython it does not give true parallel bytecode execution across cores. The GIL forces those threads to take turns executing Python-level work.

Why I/O-Bound Threads Still Help

The GIL is much less painful for programs waiting on the network, disk, or sockets. During many blocking I/O operations, the interpreter can release the GIL so another thread can run.

That means threads still work well for tasks such as:

  • downloading many URLs
  • waiting on database responses
  • handling socket-based servers
  • performing mixed I/O and light CPU work

The limitation is mainly about CPU-bound Python execution, not about all concurrency.

Common Workarounds

When CPU parallelism matters, Python developers usually choose one of these approaches:

  • 'multiprocessing to use separate processes and separate interpreters'
  • native extensions that release the GIL during heavy work
  • vectorized libraries such as NumPy that do much of the real work in native code
  • 'asyncio for high-concurrency I/O workloads without many threads'

Here is a simple multiprocessing example:

python
1from multiprocessing import Pool
2
3def square(x):
4    return x * x
5
6if __name__ == "__main__":
7    with Pool(4) as pool:
8        print(pool.map(square, [1, 2, 3, 4]))

Because separate processes have separate interpreters, they do not share one GIL.

Why Python Has Not Simply Removed It

Removing the GIL is not just deleting one lock. It changes memory management costs, extension-module assumptions, C API behavior, and performance characteristics across the ecosystem.

That is why the question "why not just remove it" has always had a complicated answer. A no-GIL interpreter can be built, but making it fast, compatible, stable, and broadly ecosystem-friendly is much harder than the slogan suggests.

Common Pitfalls

The biggest mistake is assuming threads are useless in Python. They are often quite effective for I/O-bound workloads.

Another issue is expecting a CPU-bound thread pool to scale linearly across cores in CPython. The GIL prevents that at the Python bytecode level.

Developers also confuse "Python has a GIL" with "all Python implementations behave the same way." The GIL discussion is mainly about CPython.

Summary

  • The GIL allows only one CPython thread at a time to execute Python bytecode.
  • It exists largely to simplify interpreter internals and object memory management.
  • CPU-bound Python threads do not get true parallel bytecode execution.
  • I/O-bound threading can still work well because the GIL is often released during blocking operations.
  • For CPU parallelism, use processes, native code that releases the GIL, or other concurrency strategies.

Course illustration
Course illustration

All Rights Reserved.