AnyLearn
All lessons
Programmingadvanced

CPython: the GIL and free-threading

Why CPython has a Global Interpreter Lock, what it does and does not protect, and how the three concurrency models differ. Then how PEP 703 free-threading removes the GIL using per-object locks and biased, deferred, and immortal reference counting, plus the tradeoffs that keep it opt-in.

Updated · AI-authored, review-gated · how lessons are made

Not signed in: your progress and quiz score won't be saved.
Progress1 / 9

What the GIL is

The Global Interpreter Lock is a single mutex inside the CPython interpreter. Any thread that wants to execute Python bytecode must first hold it, so only one thread runs Python code at a time, even on a 32-core machine. It is a property of the CPython implementation, not the language: Jython and IronPython have no GIL.

import threading

counter = 0
def work():
    global counter
    for _ in range(1_000_000):
        counter += 1     # load, add, store: three separate bytecodes

ts = [threading.Thread(target=work) for _ in range(4)]

Running those four threads does not use four cores for the loop; they take turns holding the GIL. For CPU-bound Python, threads give concurrency (interleaving) but not parallelism (running at the same instant).

Full lesson text

All 9 steps on one page, for reading, reference, and search.

Show

1. What the GIL is

The Global Interpreter Lock is a single mutex inside the CPython interpreter. Any thread that wants to execute Python bytecode must first hold it, so only one thread runs Python code at a time, even on a 32-core machine. It is a property of the CPython implementation, not the language: Jython and IronPython have no GIL.

import threading

counter = 0
def work():
    global counter
    for _ in range(1_000_000):
        counter += 1     # load, add, store: three separate bytecodes

ts = [threading.Thread(target=work) for _ in range(4)]

Running those four threads does not use four cores for the loop; they take turns holding the GIL. For CPU-bound Python, threads give concurrency (interleaving) but not parallelism (running at the same instant).

2. Why it exists

The GIL is not an accident to apologize for; it buys real simplicity. Reference counting touches a counter on every object operation. Without a global lock, each increment and decrement would need an atomic instruction or a per-object lock, which slows the single-threaded path that most Python code runs on.

The GIL also protects the interpreter's own mutable state, dict internals, the small-object allocator, module state, without fine-grained locks scattered everywhere. And it makes C extensions far easier to write: an author can assume their code will not be preempted by another Python thread mid-operation. For decades this trade, fast single-threaded execution and simple C interop in exchange for no in-process parallelism, was judged worth it.

3. What it means in practice

The GIL blocks parallelism for CPU-bound Python, but two escape hatches matter.

First, the GIL is released around blocking I/O: while a thread waits on a socket, disk, or time.sleep, it drops the lock so others run. So threads do give real speedups for I/O-bound work: many concurrent requests, file reads, database calls.

Second, C extensions can release it during heavy computation. NumPy, hashlib, compression, and image libraries wrap long C loops so a matrix multiply on one thread runs alongside Python on another.

from concurrent.futures import ThreadPoolExecutor
# I/O-bound: threads help because the GIL is dropped while waiting
with ThreadPoolExecutor(8) as ex:
    results = list(ex.map(fetch_url, urls))

Rule of thumb: threads for I/O, processes for CPU.

4. Three concurrency models

CPython offers three ways to run work concurrently, and the GIL is why the choice matters.

modelparallel CPU?memorybest for
threadingno (shares the GIL)sharedI/O-bound, blocking calls
multiprocessingyes (separate processes)separate, copiedCPU-bound pure Python
asynciono (one thread)sharedvery many I/O waits

multiprocessing sidesteps the GIL by running separate interpreter processes, each with its own GIL, communicating by pickling data across pipes, which costs serialization and memory. asyncio runs a single thread with an event loop, switching cooperatively at await points, ideal for tens of thousands of open sockets. Choose by the bottleneck: waiting favors threads or async, computing favors processes.

5. How the GIL hands off

A thread does not hold the GIL forever. Periodically the running thread hits the eval breaker, a check in the evaluation loop, and if another thread is waiting it releases and reacquires the lock. The cadence is the switch interval, about 5 milliseconds by default.

import sys
sys.getswitchinterval()      # 0.005
sys.setswitchinterval(0.010) # check less often

The handoff happens between bytecode instructions, never inside one, but a single Python statement is many instructions. counter += 1 compiles to a load, an add, and a store; two threads can interleave so both load the same value before either stores, and an update is lost. The GIL makes each bytecode effectively atomic, not each line. Compound updates still need a threading.Lock.

6. Free-threading: making the GIL optional

PEP 703 adds a build of CPython, the free-threaded build, where the GIL is disabled and multiple threads execute Python bytecode truly in parallel. It first shipped as an experimental, compile-time option in 3.13 and reached officially supported status in 3.14 under PEP 779, with any default-on switch left for later years.

Removing the GIL means everything it implicitly protected now needs explicit protection. Free-threading replaces the one big lock with fine-grained per-object locks plus atomic operations on shared interpreter state. Operations on a dict or list that used to be safe purely because 'only one thread runs' now take a lightweight per-object lock instead. The stated goal is real multi-core scaling for CPU-bound Python without changing what your code means.

7. Reference counting without a GIL

The hardest part of dropping the GIL is reference counting: a naive shared counter would need an atomic add on every increment and decrement, which is slow and causes cache-line contention when many threads touch the same hot object. The free-threaded build combines three techniques.

  • Biased reference counting: each object has an owner thread that updates the count with cheap non-atomic ops on a fast path, while other threads use a separate atomic path. Most objects are touched mainly by their owner.
  • Deferred reference counting: references held on interpreter stacks are not counted continuously, cutting churn on very hot objects.
  • Immortal objects: singletons such as None, True, False, small ints, and interned strings are marked immortal; their counts never change, so threads never contend on them.

Together these keep the common path fast while making counting correct across cores.

8. Tradeoffs and status

Free-threading is not free, which is why it stayed opt-in.

  • Single-threaded overhead: the extra bookkeeping makes the no-GIL build somewhat slower on one thread than the standard build, a gap later releases keep narrowing.
  • C-extension compatibility: extensions written assuming the GIL must be audited and rebuilt for the free-threaded ABI; unmodified C code can crash or corrupt state under true parallelism, so the ecosystem migrates gradually.
  • A new bug class: code that was accidentally safe 'because the GIL' can now race, so existing threaded Python may need real locks it previously got away without.
you havestandard buildfree-threaded build
I/O-bound threadsfinefine
CPU-bound pure-Python threadsserialized by the GILrun in parallel
single-thread speedbaselineslightly slower, improving

The GIL is becoming a choice rather than a fixed fact of CPython.

9. Choosing a concurrency model

The decision hinges on the bottleneck and, now, on which build you run. Waiting work parallelizes under the GIL; computing work needs processes or the free-threaded build.

flowchart TD
  A["concurrent work"] --> B["bottleneck: I/O or CPU?"]
  B --> C["I/O bound"]
  B --> D["CPU bound"]
  C --> E["threading or asyncio: GIL released while waiting"]
  D --> F["multiprocessing: separate interpreters, own GIL each"]
  D --> G["free-threaded build: parallel threads, no GIL"]

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. What does the GIL guarantee on the standard CPython build?
    • Each full line of Python executes atomically
    • Only one thread executes Python bytecode at any moment
    • Threads cannot share memory with each other
    • C extensions can never run while Python runs
  2. For a CPU-bound, pure-Python workload on the standard build, which gives real parallelism?
    • multiprocessing, via separate interpreter processes
    • threading
    • asyncio
    • None of these can help at all
  3. Why is `counter += 1` across threads still a race even with the GIL held?
    • The GIL protects reads but not writes
    • Integers become unhashable across threads
    • It compiles to separate load/add/store bytecodes and the GIL can switch between them
    • The switch interval is configured too long
  4. In the PEP 703 free-threaded build, why are None, True, and small ints made 'immortal'?
    • To save a small amount of memory
    • Because they cannot be garbage collected anyway
    • To make them hash faster
    • So their reference counts never change and threads never contend on those hot objects
  5. On the standard build, threads give a genuine speedup for which workload?
    • Multiplying large matrices in a pure-Python loop
    • Downloading many URLs concurrently, an I/O-bound task
    • A tight pure-Python numeric loop
    • Sorting a huge list purely in Python

Related lessons

Programming
advanced

CPython: reference counting and the cyclic GC

How CPython reclaims memory: immediate reference counting for the common case, plus a generational cyclic garbage collector that catches the reference cycles counting cannot. Covers what changes a refcount, generations and thresholds, the gc module, __del__ and weakref, and why a process's memory does not always shrink.

9 steps·~14 min
Programming
advanced

CPython: bytecode and the interpreter loop

What actually runs when you run Python: the compiler turns source into a code object of bytecode, and a single C evaluation loop executes it on a stack machine, one frame per call. Covers code objects, the dis module, the value stack, frames, and the adaptive specializing interpreter.

9 steps·~14 min
Programming
advanced

CPython: the object and data model

How Python values really work under the hood: every value is a heap object with a type and a reference count, names are references not boxes, and behavior comes from the data model's dunder methods. Covers identity versus equality, attribute lookup order, descriptors, and __slots__.

9 steps·~14 min
Programming
advanced

Goroutine Leaks: Who Stops This Thing

Starting a goroutine is one keyword; stopping one is a design decision nothing forces you to make. A goroutine blocked forever is never collected and holds everything it references, which is the most common Go-specific bug. This lesson covers how leaks happen, the cancellation mechanism that prevents them, and how to detect the ones already there.

8 steps·~12 min