For a long time, Python developers have heard the same advice when discussing CPU-heavy multithreading:
The GIL prevents multiple Python threads from executing Python code at the same time.
That statement is still true for the normal CPython build.
But Python now has another option.
CPython supports free-threaded builds, where the Global Interpreter Lock, or GIL, can be disabled. This allows multiple threads to execute Python code in parallel across different CPU cores.
That sounds like a simple performance upgrade.
It is not.
Removing the GIL changes some of the assumptions that Python applications and C extensions have relied on for years. A program that works perfectly with the normal interpreter may behave differently, perform differently, or expose thread-safety problems when running without the GIL.
Python 3.15 continues this work and adds more support around free-threading, including a Stable ABI variant for free-threaded builds.
So the interesting question for developers is not:
"Can I remove the GIL?"
It is:
"What should I test before I run my application without the GIL?"
What Is a Free-Threaded Python Build?
A normal CPython build uses the Global Interpreter Lock.
The GIL ensures that only one thread at a time can execute Python code in the interpreter.
For example:
import threading
def worker():
for _ in range(10_000_000):
calculate()
threads = [
threading.Thread(target=worker),
threading.Thread(target=worker),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
On a normal GIL-enabled CPython build, these threads do not execute Python bytecode truly in parallel.
They can still be useful for I/O-bound work because the interpreter releases the GIL around blocking operations.
A free-threaded build changes that model.
When the interpreter runs without the GIL, multiple Python threads can execute Python code simultaneously on different CPU cores.
Python's documentation describes free-threading as allowing full use of available CPU resources through parallel thread execution, although the actual benefit depends on the workload.
Python 3.15 Is Not the First Version With Free Threading
Free-threaded CPython was introduced in Python 3.13.
It remains an optional build rather than the normal Python installation.
You can build CPython with:
./configure --disable-gil
The resulting interpreter is a free-threaded build.
Python's configuration documentation identifies --disable-gil as the option that enables free-threaded execution. It also adds the t ABI flag to identify the build.
The important point is that installing normal Python 3.15 does not mean the GIL has disappeared.
You need to use a free-threaded build.
How to Check Whether the GIL Is Disabled
Python provides a way to check the runtime state.
For example:
import sys
print(sys._is_gil_enabled())
If the GIL is disabled, the result is:
False
If the GIL is enabled:
True
You can also inspect the interpreter version:
python -VV
Free-threaded builds identify themselves as such.
For build-level decisions, Python also exposes:
import sysconfig
print(sysconfig.get_config_var("Py_GIL_DISABLED"))
This distinguishes a build that supports free threading from a normal build.
That distinction matters because a free-threaded build can still run with the GIL enabled in some situations.
Free-Threaded Does Not Mean "No GIL Forever"
One of the easiest misconceptions is to think that a free-threaded interpreter can never use the GIL.
That is not necessarily true.
A free-threaded build can be configured to run with the GIL enabled.
For example:
PYTHON_GIL=1 python app.py
or:
python -X gil app.py
This can be useful when testing the same free-threaded build under both execution modes.
It gives you a useful comparison:
Same Python build
│
├── GIL enabled
│
└── GIL disabled
That is much better for benchmarking than comparing two completely different application environments.
Why Removing the GIL Can Improve CPU Performance
Consider a CPU-heavy function:
def calculate():
total = 0
for i in range(10_000_000):
total += i * i
return total
Now run it across multiple threads:
import threading
threads = [
threading.Thread(target=calculate),
threading.Thread(target=calculate),
threading.Thread(target=calculate),
threading.Thread(target=calculate),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
On a normal GIL-enabled interpreter, the threads cannot all execute Python code simultaneously.
On a free-threaded interpreter, they can potentially run in parallel.
If the machine has multiple CPU cores and the workload scales well, this can provide a meaningful performance improvement.
But there are several conditions hidden inside the phrase "can potentially."
The code must actually benefit from parallelism.
The threads must have enough work to do.
The workload must not spend most of its time waiting.
And the code must be safe when multiple threads access shared data simultaneously.
The First Thing to Test: Your Real Workload
Do not start by benchmarking a synthetic loop and assuming your application will get the same improvement.
A real application may spend time on:
Database
Network
Disk
Python computation
C extensions
Serialization
Locks
Queues
External services
Only some of these benefit directly from free-threaded execution.
For example, if an application spends 80% of its time waiting for a database, removing the GIL will not suddenly make the database respond faster.
The first test should therefore be:
What part of the application is CPU-bound and currently limited by Python thread execution?
Benchmark GIL-Enabled vs GIL-Disabled
Create a repeatable benchmark.
For example:
import time
import threading
def worker():
total = 0
for i in range(5_000_000):
total += i * i
return total
def run():
threads = [
threading.Thread(target=worker)
for _ in range(4)
]
start = time.perf_counter()
for thread in threads:
thread.start()
for thread in threads:
thread.join()
return time.perf_counter() - start
print(f"Elapsed: {run():.3f}s")
Run it with the GIL enabled:
PYTHON_GIL=1 python benchmark.py
Then run it with the GIL disabled:
PYTHON_GIL=0 python benchmark.py
Do not treat the result as a production benchmark.
It is only an initial experiment.
For meaningful results, use your application's real workload and run each scenario multiple times.
Single-Threaded Performance Matters Too
One of the most important free-threading trade-offs is that removing the GIL is not free.
The free-threaded interpreter has additional synchronization and memory-management work.
Python's documentation notes that free-threaded builds can have higher single-threaded overhead. The documented overhead varies by platform and workload; the pyperformance results cited by Python show a range rather than a universal fixed penalty.
This creates an important question:
Does the application's multithreading benefit outweigh the single-threaded cost?
For example:
GIL build
Single-threaded:
100 units
Free-threaded build
Single-threaded:
94 units
Four-thread workload:
350 units
The free-threaded version may be a great choice for the multi-threaded workload.
But if your application mostly executes one thread, you might not benefit.
This is why both single-threaded and multi-threaded benchmarks matter.
Test C Extensions Before Switching
This is probably the biggest compatibility issue.
Python applications often depend on packages containing native C or C++ extension modules.
Examples include libraries for:
Numerical computing
Image processing
Database drivers
Cryptography
Compression
Machine learning
Scientific computing
A free-threaded interpreter needs those extensions to be safe without relying on the GIL.
Python's extension documentation requires extensions to explicitly indicate whether they support running without the GIL. If an extension does not declare free-threading support, importing it can cause the GIL to be enabled at runtime.
That means this can happen:
Free-threaded Python
↓
Import extension
↓
Extension does not support free threading
↓
GIL becomes enabled
Your application may still run.
But you may no longer get the parallel execution you expected.
Check Which Packages Enable the GIL
This makes dependency testing extremely important.
Suppose your application uses:
application
├── numpy
├── database-driver
├── image-library
└── custom-extension
One unsupported native dependency can affect the runtime behavior of the application.
Do not assume:
Python supports free threading
=
Every package supports free threading
Those are different things.
For each important dependency, check whether the version you plan to deploy supports the free-threaded interpreter.
Pure Python Code Still Needs Thread-Safety Review
Another misconception is that the GIL was a synchronization mechanism that made all Python code thread-safe.
It was not a replacement for proper synchronization.
Consider:
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1
Running this from multiple threads requires careful reasoning about shared state.
With a free-threaded interpreter, more operations can genuinely happen at the same time.
Code that accidentally depended on the GIL for timing or serialization can expose bugs.
Use explicit synchronization:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
The general rule is simple:
If multiple threads share mutable state, make the synchronization explicit.
Do Not Rely on Internal Locking
Free-threaded CPython has internal mechanisms to protect built-in objects such as dictionaries and lists.
That does not mean you should treat every compound operation as automatically thread-safe.
For example:
if key not in cache:
cache[key] = calculate(key)
This is a multi-step operation.
Even if individual dictionary operations have internal protection, the entire sequence is not automatically an atomic transaction.
Use synchronization when the application requires it:
with lock:
if key not in cache:
cache[key] = calculate(key)
Python's free-threading documentation recommends using synchronization primitives such as threading.Lock rather than depending on internal locking behavior of built-in containers.
Test Shared Mutable State
Create tests specifically for concurrent access.
Look for:
Global variables
Caches
Lists
Dictionaries
Sets
Singleton objects
Shared queues
Connection pools
Lazy initialization
Statistics counters
For example:
cache = {}
def get_value(key):
if key not in cache:
cache[key] = calculate(key)
return cache[key]
This should be reviewed carefully before running under free threading.
A safer design might use a lock:
import threading
cache = {}
cache_lock = threading.Lock()
def get_value(key):
with cache_lock:
if key not in cache:
cache[key] = calculate(key)
return cache[key]
The right design depends on the workload, but the important point is that concurrency should be intentional.
Race Conditions May Be Hard to Reproduce
Threading bugs are often intermittent.
You might run:
python test.py
one hundred times and see no failure.
Then production fails once under a particular workload.
That is why free-threading testing should include stress tests.
For example:
from concurrent.futures import ThreadPoolExecutor
def task():
return process_data()
with ThreadPoolExecutor(max_workers=16) as executor:
results = list(
executor.map(lambda _: task(), range(10_000))
)
Increase:
Thread count
Number of operations
Shared-state access
Request concurrency
Duration
The goal is to expose timing-sensitive problems.
Test Thread Pools Carefully
Applications often use:
ThreadPoolExecutor
Free threading can make CPU-bound workloads more attractive for thread pools.
Join the conversation! Your thoughts help the community grow.