Despite a lot of talk about the removal of the GIL in Python, it is actually still the default execution mechanism in Python. But what is it and what does it do?
from concurrent.futures import ThreadPoolExecutor
def work1():
total = 0 # local total, not shared with work2
for _ in range(100):
total -= 1 # new total is made, ref-count update
print(total)
def work2():
total = 0 # local total, not shared with work1
for _ in range(100):
total += 1 # new total is made, ref-count update
print(total)
with ThreadPoolExecutor() as executor:
executor.submit(work1)
executor.submit(work2)
From looking at this code, you might expect the Python Interpreter to start and run two process threads and the output suggests it does:
.
77
-45
78
79
80
-46
-47
.
But actually, the threads run in a single process, giving control to each other at regular intervals. This works somewhat like asyncio but with two important differences:
Since the interpreter must lock the GIL when running threads, using multiple threads for CPU-bound Python code does not achieve true parallelism, and might even hurt performance due to the overhead of task switching.
The GIL protects the interpreter, not your code. When bytecode runs in two threads simultaneously:
Without the GIL, memory corruption could occur and memory safety cannot be guaranteed.
If Python bytecode is executing, the GIL must be held. But there are exceptions.
Threads can release the GIL during blocking I/O operations to allow another thread to run simultaneously. Examples include:
When the GIL is removed or made optional, Python will achieve true parallelism for CPU-bound code. To make this possible:
Written by Loek van den Ouweland on Feb. 4, 2026.