Python developers have discussed the Global Interpreter Lock, commonly called the GIL, for years.
The reason is simple: the GIL has historically limited how multiple Python threads execute CPU-bound Python code at the same time in CPython.
Python has been moving toward an optional free-threaded execution model, where the GIL can be disabled. Python 3.15 continues that work and makes free-threaded CPython more practical, although it remains an optional build rather than the default Python runtime.
This creates an important question for developers:
If the GIL is disabled, does Python code automatically become faster?
No.
Free-threading changes how Python threads can execute. Whether an application becomes faster depends on the workload, the number of threads, synchronization, native extensions, memory behavior, and how much actual CPU-bound Python work the application performs.
This article explains what free-threading means in Python 3.15 and how to evaluate it in a real application.
What Is the GIL?
The Global Interpreter Lock is a synchronization mechanism used by the standard CPython build.
In a simplified model, multiple Python threads may exist:
Thread 1
Thread 2
Thread 3
Thread 4
↓
GIL
↓
CPython interpreter
Only one thread at a time can execute Python bytecode under the traditional GIL model.
This does not mean Python cannot perform concurrent work.
Threads can still be useful when they spend significant time waiting for:
Network operations
Database operations
File I/O
Other blocking operations
For example:
import threading
def download(url):
# Network-bound work
...
threads = [
threading.Thread(target=download, args=(url,))
for url in urls
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
While one thread waits for I/O, another thread can make progress.
The problem becomes more visible with CPU-bound Python code.
CPU-Bound vs I/O-Bound Work
Consider a CPU-heavy function:
def calculate():
total = 0
for i in range(50_000_000):
total += i * i
return total
If several threads execute this function under the traditional GIL model, they do not get true parallel execution of Python bytecode across CPU cores.
A machine with:
CPU cores: 16
Python threads: 16
does not automatically mean:
16 Python threads
↓
16 cores working on Python bytecode
The GIL prevents that in the traditional CPython build.
Free-threaded CPython changes this model.
What Is Free-Threaded Python?
A free-threaded build removes the GIL so multiple Python threads can execute Python code concurrently.
The conceptual difference is:
Traditional CPython
Thread A ──┐
Thread B ──┤
Thread C ──┼── GIL ── Python execution
Thread D ──┘
versus:
Free-threaded CPython
Thread A ──→ CPU core
Thread B ──→ CPU core
Thread C ──→ CPU core
Thread D ──→ CPU core
The second model allows true parallel execution of Python code when the workload and hardware make that useful.
But removing the GIL creates new synchronization requirements inside the interpreter.
The free-threaded implementation therefore uses other mechanisms to maintain memory and object safety.
Python 3.15 and Free-Threading
Python 3.15 continues the free-threaded work introduced in previous Python releases.
The official Python documentation describes free-threaded CPython as an optional build configuration and notes that it is not enabled by default.
This distinction is important.
Installing ordinary Python 3.15 does not mean that the GIL is automatically gone.
You need a free-threaded build.
Depending on the distribution, the executable may be identifiable as a free-threaded build using the t ABI designation.
You can inspect the interpreter configuration with:
import sys
print(sys.version)
print(sys._is_gil_enabled())
On a free-threaded build where the GIL remains disabled, the latter reports False. The API is intended to allow applications and tools to determine whether the GIL is enabled.
The GIL Can Still Be Enabled
Free-threaded CPython does not necessarily mean that the GIL can never exist.
Python's free-threaded implementation provides mechanisms that can enable the GIL in certain situations, including when extension modules that require the GIL are imported.
That means developers should not assume:
Free-threaded build
=
GIL always disabled
A better model is:
Free-threaded build
↓
GIL support removed from the default execution path
↓
Some compatibility scenarios may enable it
This matters when evaluating third-party packages.
Why Removing the GIL Is Not a Free Performance Upgrade
It is tempting to think:
No GIL
+
More CPU cores
=
Faster Python
Real applications are more complicated.
A multithreaded application can spend time waiting for:
Locks
Memory
Cache coherence
Database responses
Network operations
Other threads
Allocation
Garbage collection
If your program is mostly waiting on a database, removing the GIL may have little effect.
For example:
Request
↓
Database query
↓
500 ms waiting
↓
Response
The Python interpreter is not the main bottleneck.
Free-threading is much more interesting when several threads perform substantial CPU-bound Python work.
A Simple CPU-Bound Example
Consider:
def calculate(start, end):
total = 0
for number in range(start, end):
total += number * number
return total
We can divide the work:
0 ───────── 25M
25M ─────── 50M
50M ─────── 75M
75M ─────── 100M
and give each section to a different thread.
Conceptually:
import threading
results = []
def worker(start, end):
results.append(calculate(start, end))
threads = [
threading.Thread(
target=worker,
args=(0, 25_000_000)
),
threading.Thread(
target=worker,
args=(25_000_000, 50_000_000)
),
]
With a traditional GIL-enabled interpreter, these threads cannot execute Python bytecode in true parallel fashion.
With a free-threaded interpreter, they can potentially execute concurrently on different CPU cores.
But this example also introduces a shared list:
results.append(...)
That brings us to an important issue.
Free-Threading Makes Synchronization More Important
When multiple threads can execute Python code simultaneously, shared mutable state becomes more important.
Consider:
counter = 0
def worker():
global counter
for _ in range(100_000):
counter += 1
Running this concurrently should not be designed around the assumption that every multi-step operation is automatically safe.
The correct solution is to use explicit synchronization where required.
For example:
import threading
counter = 0
lock = threading.Lock()
def worker():
global counter
for _ in range(100_000):
with lock:
counter += 1
The lock makes the critical section explicit.
Free-threading therefore changes an important design assumption:
Code that depended on the GIL for accidental synchronization should not be treated as thread-safe.
The GIL Was Never a General Thread-Safety Guarantee
This point is easy to misunderstand.
The GIL did not make arbitrary Python programs thread-safe.
Developers still needed locks and other synchronization mechanisms when sharing mutable state.
However, some existing code may have relied on implementation behavior that happened to be protected by the GIL.
Free-threaded Python exposes those assumptions more clearly.
For example:
shared = {}
def worker(key, value):
shared[key] = value
If multiple threads access shared data, you should understand the thread-safety guarantees of the specific operation and data structure rather than assuming that the GIL protects everything.
For complex shared state, explicit synchronization is usually easier to reason about.
What Happens to C Extensions?
Third-party native extensions are one of the biggest considerations when evaluating free-threaded Python.
A package containing native code may have assumptions based on the traditional GIL.
Python's free-threading documentation explains that extension modules may need to be updated to support free-threaded execution. Extensions can declare whether they support the free-threaded build, and importing an extension that does not support it can cause the GIL to be enabled.
This creates a compatibility chain:
Python application
↓
Python package
↓
Native extension
↓
Free-threaded support?
↓
Yes / No
A single dependency can therefore affect the behavior of the complete application.
Check Your Dependencies Before Migrating
Suppose an application contains:
FastAPI
↓
Database driver
↓
Native extension
↓
System library
Moving the application to a free-threaded interpreter means every relevant component needs to be evaluated.
Check:
Python package version
Native extension support
Wheel availability
Build system
CI environment
Production container
Monitoring tools
Debugging tools
Do not benchmark only your application code.
Benchmark the complete application stack.
Free-Threading and asyncio
Free-threading does not replace asynchronous programming.
Consider:
async def fetch_data():
result = await database_call()
return result
asyncio is designed primarily around cooperative concurrency and asynchronous I/O.
Free-threading addresses a different problem:
asyncio
↓
Efficient concurrent I/O
Free-threading
↓
Parallel Python execution across threads
They can also coexist.
For example, an asynchronous application may use worker threads for CPU-heavy tasks.
The important thing is to choose the concurrency model based on the workload.
Free-Threading and multiprocessing
Python's multiprocessing module has traditionally been one way to achieve parallel CPU execution by using separate processes.
Conceptually:
Process 1 → CPU core
Process 2 → CPU core
Process 3 → CPU core
Process 4 → CPU core
Each process has its own interpreter and memory space.
Free-threading provides another model:
One process
↓
Multiple Python threads
↓
Multiple CPU cores
This can reduce some of the complexity associated with multiple processes, especially when threads need to share memory.
However, shared memory also creates synchronization concerns.
Neither approach is universally better.
Memory Is Another Consideration
Multiple threads operating in one process can share objects directly.
That can be convenient:
Thread A ─┐
Thread B ─┼── Shared objects
Thread C ─┘
But shared objects can also increase contention.
With multiprocessing:
Process A → Memory A
Process B → Memory B
Process C → Memory C
isolation is stronger, but moving data between processes can introduce serialization and communication overhead.
Free-threading changes the tradeoff rather than eliminating it.
How to Benchmark Free-Threaded Python
Do not compare two interpreters by running a single command once.
A meaningful test should measure the actual workload.
For example:
Benchmark
──────────────
Input size
Thread count
Execution time
CPU utilization
Throughput
Memory usage
Test several thread counts:
1 thread
2 threads
4 threads
8 threads
16 threads
You may see something like:
Threads Relative throughput
1 baseline
2 improved
4 improved
8 improved
16 little additional gain
The exact result depends entirely on the workload.
The important thing is to find where additional concurrency stops providing useful work.
Use a Representative Workload
A benchmark like:
sum(range(10_000_000))
may be useful for experimentation.
It is not enough to decide whether a production service should migrate.
A better test resembles the actual workload:
Production workload
↓
Representative dataset
↓
Real dependencies
↓
Real thread count
↓
Realistic request pattern
If your production application spends most of its time waiting on PostgreSQL, benchmark that behavior.
If it performs CPU-heavy image processing, benchmark the image-processing workload.
Measure More Than Execution Time
Suppose free-threaded Python changes:
Execution time:
10 sec → 7 sec
That looks useful.
But also measure:
CPU usage
Memory usage
Lock contention
Throughput
Tail latency
Error rate
Dependency behavior
A change that reduces CPU time but causes significantly higher memory usage may not be appropriate for a constrained production environment.
Similarly, average latency can improve while p99 latency becomes worse.
Production decisions should use the metrics that matter to the application.
A Practical Migration Strategy
Do not replace the production interpreter immediately.
Use stages.
Stage 1: Inventory
Identify:
Python version
Native extensions
Critical dependencies
Thread usage
Shared state
CPU-heavy operations
Stage 2: Run Tests
Run the existing test suite against the free-threaded interpreter.
Look for:
Crashes
Import failures
Race conditions
Incorrect results
Performance regressions
Native extension issues
Stage 3: Run Concurrency Tests
Increase thread counts gradually.
1
2
4
8
16
Observe where performance improves and where contention appears.
Stage 4: Profile
Use appropriate CPU and wall-clock profiling.
Python 3.15's profiling improvements and frame-pointer defaults can help system-level profiling on supported platforms.
Stage 5: Canary
Deploy the free-threaded build to a small portion of traffic.
Monitor:
Error rate
Latency
CPU
Memory
Throughput
Dependency failures
Stage 6: Compare
Compare the same workload against the traditional interpreter.
Only then decide whether the free-threaded build provides enough value for the application.
Common Mistakes
Assuming More Threads Always Mean More Performance
Thread count and performance are not directly proportional.
Too many threads can increase contention and scheduling overhead.
Assuming Free-Threading Fixes Every CPU Problem
A slow algorithm remains a slow algorithm.
Changing the interpreter does not replace algorithmic optimization.
Ignoring Native Dependencies
A single unsupported extension can affect free-threaded behavior.
Relying on the GIL for Synchronization
If correctness depends on accidental GIL behavior, the code should be reviewed.
Benchmarking Only One Thread Count
Free-threading is specifically about concurrent execution.
Always test multiple levels of concurrency.
Testing Only Synthetic Code
A microbenchmark can demonstrate potential.
A representative workload determines practical value.
Migrating Production First
Run compatibility and performance testing before changing the production runtime.
Best Practices
When evaluating free-threaded Python 3.15:
Treat it as a separate runtime configuration.
Inventory native dependencies before migration.
Run the complete test suite.
Review code that shares mutable state across threads.
Use explicit synchronization where required.
Benchmark realistic CPU-bound workloads.
Test multiple thread counts.
Measure memory and CPU usage as well as execution time.
Profile both Python and native execution where appropriate.
Test third-party packages independently.
Use a staged or canary deployment for production evaluation.
Keep a GIL-enabled deployment path available during migration.
Compare tail latency, not just averages.
Do not assume free-threading automatically improves I/O-bound applications.
Document which dependencies support the free-threaded runtime.
When Free-Threading Makes Sense
Free-threaded Python is particularly interesting when an application has:
CPU-heavy Python work
+
Multiple independent tasks
+
Multiple CPU cores
+
Thread-based architecture
Examples can include:
CPU-heavy data processing
Parallel transformations
Some scientific workloads
Image processing
Large in-memory computations
Certain server-side workloads
The exact benefit depends on the implementation and dependencies.
When It May Not Help Much
Free-threading may provide limited benefit when the workload is primarily:
Network waiting
Database waiting
File I/O
External API calls
For example:
Python
↓
HTTP request
↓
400 ms waiting
Removing the GIL does not make the remote server respond faster.
Likewise, if your application already uses asynchronous I/O efficiently, changing to free-threaded execution may not provide a meaningful improvement for that particular workload.
Advantages and Limitations
Advantages
Multiple Python threads can execute Python code concurrently.
Better utilization of multiple CPU cores for suitable workloads.
Can provide an alternative to process-based parallelism.
Shared memory between threads can simplify some architectures.
Opens new possibilities for CPU-bound multithreaded Python applications.
Limitations
Free-threaded Python is not the default CPython build.
Some native extensions may not support it.
Shared mutable state requires careful synchronization.
More threads do not automatically mean better performance.
Some workloads may see little improvement.
Memory and synchronization overhead can limit scalability.
Application behavior can change when code previously depended on GIL-related assumptions.
Summary
Python's move toward free-threading changes one of CPython's most important execution assumptions.
With the traditional GIL-enabled interpreter, multiple threads cannot execute Python bytecode in true parallel fashion. A free-threaded build removes that restriction and allows suitable Python workloads to use multiple CPU cores through threads.
Python 3.15 continues this work, but free-threading remains an optional runtime configuration rather than the default. Native extensions and third-party dependencies are therefore an important part of any migration plan.
The biggest mistake is to treat free-threading as an automatic performance upgrade.
The right question is:
Does my workload contain enough
CPU-bound parallel work to benefit?
If the answer is yes, test it with realistic data, multiple thread counts, real dependencies, and production-like concurrency.
If the application is primarily waiting on databases, networks, or external services, free-threading may not address the actual bottleneck.
The safest approach is to treat free-threading as a runtime option to benchmark and validate—not as a switch that automatically makes every Python application faster.

Join the conversation! Your thoughts help the community grow.