Skip to main content

What Is the Python GIL? Global Interpreter Lock Explained

Concurrency Concepts

Demystifying the GIL

The GIL lets only one thread execute Python bytecode at a time. Here's what that actually means for your code — and what to do about it.

What the GIL actually locks

The Global Interpreter Lock is a mutex inside CPython that allows only one thread to execute Python bytecode at any moment. Ten threads can exist and hold live stack frames, but only the thread holding the GIL advances Python code; the interpreter forces a handoff every few milliseconds (tunable via sys.setswitchinterval), so threads interleave rapidly rather than run simultaneously.

The reason is memory management. CPython uses reference counting — every object carries a counter that concurrent threads would corrupt. One big lock around the interpreter makes every refcount operation safe, and keeps single-threaded code (most Python ever run) faster than fine-grained locking would.

The part that changes everything: the GIL is released whenever a thread isn't executing Python bytecode.

  • Blocking I/O — network calls, file reads, time.sleep, database queries — releases it, so other threads run while one waits.
  • Heavy C-extension work — NumPy linear algebra, hashing, compression — releases it around the C loops.

So the practical rule: threads give you parallel waiting, not parallel computing.

See it in numbers

CPU-bound work — threads buy nothing:

import time
from concurrent.futures import ThreadPoolExecutor

def crunch(_=None):
return sum(i * i for i in range(10_000_000))

start = time.perf_counter()
crunch(); crunch()
print(f"serial: {time.perf_counter() - start:.1f}s") # ~1.4s

start = time.perf_counter()
with ThreadPoolExecutor() as pool:
list(pool.map(crunch, range(2)))
print(f"threads: {time.perf_counter() - start:.1f}s") # ~1.4s — no speedup

I/O-bound work — threads shine, because the GIL is free while each thread waits on the network:

from concurrent.futures import ThreadPoolExecutor
import urllib.request

def fetch(url):
return urllib.request.urlopen(url).read()

urls = ["https://example.com"] * 20

# serial: ~20 × latency; threaded: ~1 × latency
with ThreadPoolExecutor(max_workers=20) as pool:
pages = list(pool.map(fetch, urls))

Getting real parallelism

WorkloadReach forWhy
Many network calls / file opsthreading or asyncioGIL released while waiting
Thousands of connectionsasyncioOne thread, event loop multiplexes
CPU-heavy pure Pythonmultiprocessing / ProcessPoolExecutorEach process has its own GIL
CPU-heavy numeric codeNumPy / Cython / NumbaC code releases the GIL in-process

The one-line upgrade when compute is the bottleneck — same API, real cores:

from concurrent.futures import ProcessPoolExecutor  # was ThreadPoolExecutor

with ProcessPoolExecutor() as pool:
results = list(pool.map(crunch, range(8)))

Caveats: processes don't share memory, arguments are pickled across the boundary, and the entry point needs an if __name__ == '__main__': guard.

Free-threaded Python (PEP 703)

CPython now ships an official free-threaded build — the experimental python3.13t / python3.14t interpreters — that removes the GIL entirely, letting threads run Python bytecode on multiple cores at once. The trade-offs today: some single-threaded overhead, and the ecosystem is still catching up (C extensions must ship free-threaded wheels). The default CPython from python.org and OS packages still has the GIL, so for code you deploy now, the model above is the one that applies.

Frequently Asked Questions

Does the GIL make Python threads useless?

No — it makes them specialized. For I/O-bound work (APIs, scraping, file processing), threads deliver near-linear speedups because the GIL is released during every blocking call. It is only CPU-bound pure-Python code where threads add nothing; that is what multiprocessing and C extensions are for.

Do NumPy and other C extensions bypass the GIL?

Largely, yes. Well-written extensions release the GIL around long C computations, so a NumPy matrix multiply can use multiple cores (via its BLAS backend) while other Python threads keep running. This is why numeric workloads often parallelize fine inside a single process.

Is the GIL being removed?

PEP 703 made a GIL-free build of CPython official starting with 3.13, as an explicitly experimental, separately-compiled interpreter. It becomes practical for production as libraries publish free-threaded wheels. The default build keeps the GIL for now — plan around it, and treat free-threading as something to watch, not something to depend on yet.

Next up in your learning path