The GIL Explained

· 4 min read · Updated March 14, 2026 · intermediate
gil concurrency multithreading multiprocessing performance threading python-internals

Python Global Interpreter Lock (GIL) is one of the most discussed and often misunderstood features of the language. If you have ever wondered why your threaded Python code does not run as fast as you would expect, the GIL is usually the reason.

What Is the GIL?

The GIL is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecode simultaneously. In practical terms, this means that only one thread can execute Python code at any given time, even on a multi-core processor.

This might sound like a flaw, but it was a deliberate design choice with valid reasons behind it.

Why Does the GIL Exist?

The GIL exists primarily for memory safety and simplicity. Python memory management relies heavily on reference counting. Without the GIL, two threads could potentially modify the same object at the same time, leading to memory leaks or crashes.

Before the GIL, early Python implementations suffered from race conditions that were difficult to debug. Adding the GIL was a pragmatic solution that made Python more reliable at the cost of some parallelism.

The Impact on Threading

When you are writing threaded code, the GIL directly affects performance:

import threading
import time

def cpu_bound_task(n):
    """Simulate CPU-intensive work."""
    total = 0
    for i in range(n):
        total += i * i
    return total

# Single-threaded execution
start = time.time()
result = cpu_bound_task(10_000_000)
print(f"Single-threaded: {time.time() - start:.2f}s")

# Multi-threaded execution
start = time.time()
threads = []
for _ in range(4):
    t = threading.Thread(target=cpu_bound_task, args=(2_500_000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Multi-threaded: {time.time() - start:.2f}s")

Output:

Single-threaded: 1.15s
Multi-threaded: 1.23s

The multi-threaded version is actually slower due to the overhead of thread creation and the GIL contention. This is the core problem with CPU-bound threads in Python.

When Threads Still Work

The GIL does not affect I/O-bound operations because threads release the GIL while waiting for external resources:

import threading
import time
import requests

def fetch_url(url):
    """Fetch a URL (I/O-bound operation)."""
    response = requests.get(url)
    return len(response.content)

urls = [
    "https://example.com",
    "https://example.org",
    "https://example.net",
] * 10

# Sequential execution
start = time.time()
for url in urls:
    fetch_url(url)
print(f"Sequential: {time.time() - start:.2f}s")

# Threaded execution
start = time.time()
threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f"Threaded: {time.time() - start:.2f}s")

Output:

Sequential: 3.45s
Threaded: 0.52s

Threading dramatically improves performance for I/O-bound tasks because threads can release the GIL while waiting for network responses.

Working Around the GIL

For CPU-bound work, you have several alternatives:

Multiprocessing

The multiprocessing module bypasses the GIL by using separate Python processes, each with its own interpreter:

from multiprocessing import Pool

def cpu_task(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with Pool(4) as pool:
        results = pool.map(cpu_task, [10_000_000] * 4)
    print(f"Total: {sum(results)}")

Each process has its own GIL, so they can truly run in parallel.

ProcessPoolExecutor

A higher-level API for multiprocessing:

from concurrent.futures import ProcessPoolExecutor

def calculate(n):
    return sum(i * i for i in range(n))

with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(calculate, [10_000_000] * 4))

C Extensions

Native C extensions can release the GIL while performing computations, allowing other threads to run. Libraries like NumPy do this extensively.

The Future: Free-Threaded Python

Python 3.13 introduced an experimental free-threaded build that removes the GIL. This version allows true multi-threaded execution, though it comes with some performance trade-offs in single-threaded scenarios.

To use it, you need a Python 3.13+ build compiled with —disable-gil. Check if your build is free-threaded:

import sys
print(hasattr(sys, "_enablelegacygcilavailable"))

If this prints True, you are running the free-threaded version.

Choosing the Right Approach

Task TypeSolution
I/O-bound (network, files)Threading
CPU-bound (computation)Multiprocessing
Both I/O and CPUCombination or async
Performance-criticalConsider Cython or C extensions

Understanding the GIL helps you make better architectural decisions. Do not avoid threading entirely, just use it for the right workloads.

See Also