The GIL and Free-Threaded Python

Python 3.14 · ✓ verified by execution on 2026-07-31

The Global Interpreter Lock (GIL) is perhaps the most famous (and infamous) architectural feature of the standard Python implementation (CPython).

In this lesson, we will dissect exactly what the GIL is, why it exists, when it drops, and the modern PEPs working to abolish it.

Why the GIL exists

In CPython, every object is represented by a C struct (PyObject) that maintains a reference count. When you assign an object to a new variable, its reference count increases. When the variable goes out of scope, the count decreases.

If two threads were to increment the reference count of the exact same object at the exact same microsecond, a race condition could occur, causing the count to be wrong. This would lead to memory leaks or catastrophic segfaults.

To solve this, CPython introduced a single, massive lock around the entire interpreter: the GIL. Only the thread holding the GIL is allowed to execute Python bytecode.

I/O Bound vs CPU Bound

The GIL makes true CPU parallelism impossible in pure Python. However, it does allow true concurrency for I/O bound tasks.

Whenever a Python thread makes a blocking system call (like time.sleep(), requesting data from a socket, or reading a file), CPython explicitly drops the GIL.

python · visualize
import time
import threading

def blocking_io():
    # The GIL is released immediately when this C-level sleep starts
    time.sleep(0.1)

start = time.time()
t1 = threading.Thread(target=blocking_io)
t2 = threading.Thread(target=blocking_io)

# Both threads run concurrently because they yield the GIL during I/O
t1.start()
t2.start()
t1.join()
t2.join()

print(f"Elapsed: {time.time() - start:.3f}s")

If the GIL serialized everything, this would take 0.2 seconds. Because the GIL drops during I/O, it takes ~0.1 seconds!

The Free-Threaded Future

For decades, the Python community has tried to remove the GIL to unlock true multi-core CPU performance. Modern efforts have finally succeeded!

You can actually check if your runtime environment has disabled the GIL using sys._is_gil_enabled()!

Check yourself

What is the primary purpose of the Global Interpreter Lock (GIL) in CPython?

Reveal answer

To prevent multiple threads from mutating the same C-level structures (like reference counts) simultaneously, preventing memory corruption. — The GIL's primary job is to protect the CPython internal state (such as reference counting on the `PyObject` C struct) from concurrent mutation.

Which PEP introduces the experimental 'free-threaded' build of Python that removes the GIL?

Reveal answer

PEP 703 — PEP 703 introduces the `--disable-gil` build configuration for Python 3.13, marking a historic shift away from the Global Interpreter Lock.

If you are running a purely CPU-bound task in standard CPython (with the GIL active), what happens if you spawn 4 threads to execute it concurrently?

Reveal answer

The task will run at the exact same speed, or slightly slower due to thread context-switching overhead. — Because the GIL serializes bytecode execution, only one thread can do CPU work at a time. The overhead of context switching actually makes it slightly slower than running on a single thread.

Challenges

Challenge 1 +100 XP

Write a function `is_gil_active()` that returns the value of `sys._is_gil_enabled()` if available, otherwise returns `True`.

python
  • Test 1 — expects "True\n"
Show solution (0 XP)
import sys

def is_gil_active():
    if hasattr(sys, '_is_gil_enabled'):
        return sys._is_gil_enabled()
    return True

print(is_gil_active())

Challenge 2 +100 XP

The function `increment_counter` tries to increment `shared_dict['count']`. Since dictionary lookups and assignments are separate opcodes, this is not atomic. Fix it by wrapping the critical section in `with my_lock:`.

python
  • Test 1 — expects "1\n"
Show solution (0 XP)
import threading

shared_dict = {'count': 0}
my_lock = threading.Lock()

def increment_counter():
    with my_lock:
        shared_dict['count'] += 1

increment_counter()
print(shared_dict['count'])