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 timeimport threadingdef 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/Ot1.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!
PEP 684 (Python 3.12): Introduced the Per-Interpreter GIL. By isolating interpreters within the same process, they can each have their own GIL and run in parallel.
PEP 703 (Python 3.13+): Introduced the Free-Threaded Build. By compiling CPython with --disable-gil, the interpreter replaces the GIL with fine-grained locks on individual lists and dictionaries, and uses “biased reference counting” for thread-safe memory management.
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 sysdef is_gil_active(): if hasattr(sys, '_is_gil_enabled'): return sys._is_gil_enabled() return Trueprint(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:`.