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.
But increasing the number of workers indefinitely is not a good strategy.
For example:
ThreadPoolExecutor(max_workers=100)
does not automatically mean the application will be faster.
Too many threads can create:
Lock contention
Context switching
Memory overhead
CPU scheduling overhead
Queue contention
Start with a small number of workers and benchmark different configurations.
A useful test might compare:
1 worker
2 workers
4 workers
8 workers
16 workers
Then find where additional workers stop providing useful throughput.
CPU-Bound Work Is the Main Opportunity
Free threading is most interesting for workloads such as:
Image processing
Data transformation
Parsing
Compression
Numerical calculations
CPU-heavy business logic
Scientific workloads
Some machine-learning workloads
These are cases where multiple CPU cores can potentially execute useful Python work at the same time.
For I/O-bound applications, threads were already useful with the GIL because blocking operations release it.
So you may see a much smaller improvement in applications dominated by:
HTTP requests
Database queries
File I/O
Message queues
Network operations
Measure before changing architecture.
Test Memory Usage
Free-threaded builds can use more memory than normal builds.
Python's documentation lists increased memory usage among the known characteristics of free-threaded builds. Several implementation decisions contribute to this difference.
So your benchmark should measure more than execution time.
Track:
CPU
Memory
Throughput
Latency
Thread count
Context switches
Error rate
For a server, compare:
Requests per second
p50 latency
p95 latency
p99 latency
Memory per process
CPU per request
A system that is 20% faster but consumes 80% more memory may not be a good trade-off.
Test Garbage Collection and Object Allocation
Free-threaded execution changes some of the internal costs around object management.
Multiple threads can manipulate Python objects simultaneously.
The interpreter therefore needs additional synchronization and coordination.
If your application creates huge numbers of temporary objects, benchmark allocation-heavy workloads separately.
For example:
def process():
for _ in range(1_000_000):
data = {
"id": 1,
"name": "example",
"active": True,
}
transform(data)
Compare this workload under both interpreter modes.
Do not assume that a CPU-bound benchmark involving numerical loops will predict the performance of an allocation-heavy application.
Different workloads can behave very differently.
Test Context and Thread Behavior
Free-threaded builds also have behavioral differences around thread context.
Python's documentation notes that the thread_inherit_context setting defaults differently between free-threaded and GIL-enabled builds. In the free-threaded build, newly created threads inherit a copy of the caller's context by default.
This can matter for applications using:
contextvarsRequest context
Logging context
Tracing
Authentication information
Correlation IDs
If your application relies on context propagation, include it in the test suite.
Test Iterators Carefully
Shared iterators deserve special attention.
Consider:
iterator = iter(items)
and multiple threads consuming the same iterator.
Do not assume that this is safe simply because the object itself is built into Python.
Python's free-threading documentation specifically identifies concurrent access to the same iterator as an area that is generally not thread-safe.
A safer design is often to divide the work explicitly:
chunks = split_into_chunks(items)
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(process_chunk, chunks)
Each worker gets its own portion of the workload.
C Extension Authors Have More Work to Do
If you maintain a Python package with a C extension, free threading requires additional testing.
The extension needs to correctly declare whether it supports running without the GIL.
For a multi-phase module, Python provides the Py_mod_gil module slot.
For example:
static struct PyModuleDef_Slot module_slots[] = {
{
Py_mod_gil,
Py_MOD_GIL_NOT_USED
},
{0, NULL}
};
This tells CPython that the module supports operation without the GIL.
For single-phase initialization, Python provides:
PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
The exact implementation depends on how the extension is structured and which Python versions it supports.
Python's C API documentation describes these mechanisms for declaring free-threading compatibility.
Python 3.15 Adds abi3t
Python 3.15 introduces the Stable ABI for Free-Threaded Builds, called abi3t.
This is important for package authors because distributing native extensions across multiple Python versions can otherwise require many separate builds.
The normal Stable ABI uses:
abi3
The free-threaded Stable ABI uses:
abi3t
Python's 3.15 documentation describes abi3t as the Stable ABI variant for free-threaded builds.
However, adopting it is not simply a build-flag change for every extension.
C extensions may need source changes to use APIs and module initialization mechanisms compatible with the free-threaded Stable ABI.
Wheel Compatibility Matters
Normal Python wheels and free-threaded Python wheels are not always interchangeable.
You may see tags such as:
cp315
cp315t
The t identifies the free-threaded build.
For example:
package-cp315-cp315-manylinux_x86_64.whl
and:
package-cp315-cp315t-manylinux_x86_64.whl
target different interpreter configurations.
This matters when deploying applications with native dependencies.
Before moving to free-threaded Python, check whether your deployment system can install the correct wheels.
Build a Dependency Compatibility Matrix
For a serious application, create a simple table:
Package | Pure Python | Native Extension | Free-Threading Support | Tested |
|---|---|---|---|---|
Package A | Yes | No | Yes | Yes |
Package B | No | Yes | Yes | Yes |
Package C | No | Yes | No | No |
Package D | Yes | No | Yes | Yes |
The exact information will vary by package.
The purpose is to identify dependencies that may cause the GIL to be re-enabled or may otherwise have compatibility problems.
This is especially important for applications with many native dependencies.
Test the GIL Fallback Behavior
An application may appear to be running on a free-threaded interpreter while an extension has caused the GIL to become enabled.
That can invalidate a benchmark.
Check the runtime state:
import sys
print("GIL enabled:", sys._is_gil_enabled())
Run this before and after importing major dependencies.
For example:
import sys
print("Before:", sys._is_gil_enabled())
import some_native_package
print("After:", sys._is_gil_enabled())
If the GIL changes state, investigate why.
This is much better than assuming that the presence of a free-threaded interpreter automatically means the application is running without the GIL.
Do Not Rewrite Threading Code Immediately
If your existing application uses:
ThreadPoolExecutor
threading.Thread
queue.Queue
you do not necessarily need to redesign it.
Start by running the existing application under the free-threaded interpreter.
Then measure.
If the workload improves without changes, that is useful information.
If it does not, investigate the bottleneck.
If it crashes or produces inconsistent results, focus on correctness before performance.
A good migration sequence is:
Existing application
↓
Run under free-threaded Python
↓
Find compatibility problems
↓
Fix correctness issues
↓
Benchmark
↓
Tune worker counts
↓
Benchmark again
Use Processes as a Baseline
Do not compare only:
GIL vs free-threaded
Also compare against a process-based solution when appropriate.
For CPU-bound workloads, you might have:
Threads + GIL
Threads + no GIL
Processes
For example:
Approach | Shared Memory | CPU Parallelism | Startup Cost | Complexity |
|---|---|---|---|---|
Threads + GIL | Yes | Limited for Python CPU work | Low | Low |
Threads + free threading | Yes | Yes | Low | Medium |
Processes | No | Yes | Higher | Medium |
The best choice depends on the application.
Free threading is another concurrency option, not a universal replacement for multiprocessing.
A Practical Migration Checklist
Before moving a production service to a free-threaded Python build, test the following.
Runtime
Check:
import sys
print(sys._is_gil_enabled())
Verify that the interpreter is actually running without the GIL.
Dependencies
Test every important third-party package.
Pay particular attention to native extensions.
Shared State
Search for:
Global dictionaries
Global lists
Caches
Singletons
Counters
Lazy initialization
Shared iterators
Review each one for concurrency assumptions.
Threading
Test:
ThreadPoolExecutor
threading.Thread
Locks
Events
Conditions
Queues
under realistic concurrency.
Performance
Measure:
Single-thread performance
Multi-thread performance
Throughput
Latency
CPU
Memory
Correctness
Run:
Unit tests
Integration tests
Stress tests
Race-sensitive tests
Long-running tests
Deployment
Verify:
Python runtime
Container image
Native dependencies
Wheel availability
CI/CD
Monitoring
Rollback process
Do not make the production switch until the complete path has been tested.
Common Mistakes
Assuming Free Threading Automatically Makes Code Faster
It does not.
The workload has to benefit from parallel execution.
Testing Only a CPU Loop
Synthetic loops do not represent a complete production application.
Ignoring Native Dependencies
A C extension can change the runtime's GIL behavior.
Depending on Accidental Thread Safety
Code that happened to work under the GIL may need explicit synchronization.
Benchmarking Only Throughput
A faster application that consumes significantly more memory may not be a better application.
Increasing Thread Counts Too Quickly
More threads can create contention instead of improving performance.
Treating frozendict, dict, and Other Containers as Automatically Thread-Safe
Container-level implementation details do not replace application-level synchronization.
Switching Production Without a Rollback Plan
Free-threaded Python should initially be treated as a deployment experiment.
Keep the previous runtime available so you can quickly return to it if needed.
When Free-Threaded Python Is Worth Testing
Free-threaded Python is particularly interesting when your application:
Uses multiple threads.
Has CPU-heavy Python workloads.
Runs on multi-core machines.
Currently uses multiprocessing mainly to bypass the GIL.
Has well-defined shared-state boundaries.
Depends on packages that support free threading.
It may be less compelling when your application:
Is mostly single-threaded.
Is dominated by database or network latency.
Has many unsupported native dependencies.
Depends heavily on GIL-based assumptions.
Has poor synchronization around shared state.
The only reliable way to know is to test the actual application.
Final Thoughts
Free-threaded Python is one of the more significant changes in modern CPython, but it should not be treated as a simple switch that makes every application faster.
The real benefit is the ability to execute Python code concurrently across multiple CPU cores using threads without the traditional GIL restriction.
The cost is that applications and native extensions need to be ready for a more genuinely concurrent execution model.
Python 3.15 makes this ecosystem more mature. It continues free-threading support and introduces abi3t, giving extension developers a Stable ABI option specifically for free-threaded builds.
For application developers, the safest approach is practical:
Test
↓
Measure
↓
Find compatibility problems
↓
Fix synchronization issues
↓
Benchmark real workloads
↓
Compare against the current runtime
↓
Deploy gradually
Do not remove the GIL because it sounds like a performance improvement.
Remove the dependency on the GIL only when your application has been tested and the measurements show that free threading is actually useful.
Summary
Python's free-threaded builds allow CPython to run without the Global Interpreter Lock, giving multiple threads the opportunity to execute Python code in parallel.
That can be a major advantage for CPU-heavy, multi-threaded workloads, but it does not automatically make every application faster. Free-threaded builds can have additional overhead, use more memory, and expose problems in code or native extensions that relied on the traditional GIL behavior.
Before switching, test your third-party packages, especially C extensions. Check whether the GIL is actually disabled, review shared mutable state, stress-test threaded code, and compare single-threaded as well as multi-threaded performance.
The best migration strategy is to treat free threading as something to measure and validate, not simply enable. If your workload benefits from real parallel execution and your dependencies are ready, Python 3.15 gives you a much stronger foundation for using threads on multi-core systems.

Join the conversation! Your thoughts help the community grow.