The GIL in Practice
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Many people say Python cannot do two things at once. The truth is narrower, and knowing exactly where it applies tells you which tool to use.
What the GIL is
GIL, the Global Interpreter Lock, is a lock inside CPython. Only the thread holding it can run Python bytecode, so threads of one process cannot run Python code in parallel.
CPython, the usual Python, lets only one thread run Python code at any moment. A lock called the GIL enforces this.
The picture shows one lock shared by threads, and one lock per process.
Example 1 · The switch interval
import sys
print(sys.getswitchinterval())0.005
What this code does
- Python offers the GIL to another thread about every 5 ms.
- This is why a thread can be paused in the middle of
count += 1.
The GIL protects Python's own memory bookkeeping. It was kept because it makes single-threaded code fast and C extensions simple.
Waiting work: threads still help
I/O-bound work spends most of its time waiting for input or output, such as a network reply, a disk read or a database. The CPU is idle during the wait.
A thread waiting for a network reply, a file or time.sleep gives up the GIL. Other threads run while it waits.
import threadingimport time
start = time.perf_counter()threads = [threading.Thread(target=time.sleep, args=(0.2,)) for _ in range(5)]for t in threads: t.start()for t in threads: t.join()print(time.perf_counter() - start < 0.8)So many waits can overlap even with a GIL.
Example 2 · Waiting overlaps in threads
import timefrom concurrent.futures import ThreadPoolExecutor
def fetch(n): time.sleep(0.2) # stands in for a network call return n
start = time.perf_counter()with ThreadPoolExecutor(5) as ex: results = list(ex.map(fetch, range(5)))elapsed = time.perf_counter() - startprint(results)print("faster than one by one:", elapsed < 0.8)[0, 1, 2, 3, 4] faster than one by one: True
What this code does
- One by one this would take about 1 second. Threads wait together, so it takes about 0.2 s.
- The comparison prints True on any normal machine, while exact times change from run to run, so we do not print them.
Five waits of 0.2 s take about 1 s one by one, and about 0.2 s together.
Computing work: use processes
CPU-bound work spends its time calculating, so the processor is busy the whole time. Faster results need more CPU cores working in parallel, not more waiting at once.
A loop of Python arithmetic never waits, so its thread keeps the GIL. Two such threads take turns and finish no sooner than one.
Split the work across processes instead.
Example 3 · Threads and processes give the same answers
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
def total(n): return sum(i * i for i in range(n))
if __name__ == "__main__": sizes = [1000, 2000] with ThreadPoolExecutor(2) as ex: by_threads = list(ex.map(total, sizes)) with ProcessPoolExecutor(2) as ex: by_processes = list(ex.map(total, sizes)) print(by_threads == by_processes, by_threads)True [332833500, 2664667000]
What this code does
- Both executors compute the same numbers. Only the speed differs.
- For a large
n, the process version can use two cores and the thread version cannot, because of the GIL.
Libraries such as NumPy release the GIL inside their C code, so threads can help there. Pure Python loops do not.
What the GIL does not do
Switch interval is how often Python offers the GIL to another waiting thread. The default is 0.005 seconds, and sys.getswitchinterval() reports it.
The GIL does not make your code thread-safe. It can switch threads between the steps of count += 1, so races still happen.
import sys
print(sys.getswitchinterval())You still need locks around shared updates.
Example 4 · Choosing the tool
def choose(kind): tools = { "network": "threads or asyncio", "disk": "threads", "calculation": "processes", } return tools[kind]
for kind in ("network", "disk", "calculation"): print(kind, "->", choose(kind))network -> threads or asyncio disk -> threads calculation -> processes
What this code does
- The question to ask is where the time goes: waiting, or calculating.
- Many thousands of network waits are better on asyncio, which comes next.
Python 3.13 adds an experimental free-threaded build without the GIL. It is optional, and most programs still run on the normal build.
Next step
Check your understanding
10 quick questions