Why Compare Free-Threaded Python With Standard CPython?
Python developers have traditionally used threads to handle concurrent work, but the Global Interpreter Lock (GIL) has limited how much CPU-bound Python code can execute in parallel within a single process. Developers who needed parallel execution often turned to multiprocessing, distributed workers, or native extensions.
Free-threaded CPython changes that trade-off. It allows multiple threads to execute Python code in parallel by running the interpreter without the GIL. The capability has been available experimentally since Python 3.13 and continues to evolve.
With Python 3.15, developers have another reason to evaluate this approach: the release includes further improvements to free-threading support and C API compatibility. Official Python 3.15 macOS installers now include free-threading support by default, although developers must still select the appropriate interpreter and verify whether the GIL is actually disabled.
The important question is whether free-threaded Python makes a real application faster. Removing the GIL can improve CPU-bound workloads that scale across cores, but it can also introduce additional overhead, expose synchronization bugs, and create compatibility issues with native extensions.
A useful comparison therefore needs to measure more than execution time. It should also examine thread safety, memory consumption, dependency compatibility, and the amount of parallel work the application can actually perform.
What Is the Difference Between the Two Builds?
Standard CPython normally uses the GIL to coordinate access to Python objects. A thread must hold the lock before executing Python code that accesses interpreter-managed objects.
This simplifies several aspects of interpreter implementation, but it limits CPU-bound parallelism among Python threads.
A free-threaded build supports running without that lock. Instead, CPython uses other mechanisms, including internal object locks and changes to memory management, to support concurrent access.
Feature | Standard CPython | Free-threaded CPython |
|---|
GIL | Enabled by default | Can be disabled |
CPU-bound Python threads | Generally limited to one thread executing Python code at a time | Can execute Python code in parallel across cores |
I/O-bound threading | Useful for concurrent I/O | Still useful, but not automatically faster |
Single-thread performance | Generally the baseline | May have additional overhead |
Thread safety | Requires synchronization for shared mutable state | Still requires synchronization for shared mutable state |
Native extension compatibility | Broad compatibility with conventional extensions | Extensions must support free-threaded execution or may re-enable the GIL |
Best use case | General-purpose applications and workloads that do not benefit from parallel Python execution | CPU-bound threaded workloads with compatible dependencies |
Free-threaded CPython is not a separate Python language. Most Python code can run on either build, but assumptions about thread safety, dependencies, and performance need to be checked.
Understand What Happens to the GIL
One common misunderstanding is that installing a free-threaded interpreter automatically guarantees parallel execution.
The build can support free-threading while the GIL is enabled at runtime. For example, an extension module that does not declare free-threading support can cause the interpreter to enable the GIL when that module is imported.
Python exposes sys._is_gil_enabled() to check the runtime state.
import sys
import sysconfig
supports_free_threading = (
sysconfig.get_config_var("Py_GIL_DISABLED") == 1
)
if hasattr(sys, "_is_gil_enabled"):
gil_enabled = sys._is_gil_enabled()
else:
gil_enabled = True
print("Free-threaded build:", supports_free_threading)
print("GIL enabled:", gil_enabled)
The first value describes the interpreter build configuration. The second reports whether the GIL is currently enabled.
Both values matter. A free-threaded build with the GIL enabled does not provide the same parallel-execution behavior as a run with the GIL disabled.
For performance testing, record these values alongside the Python version, operating system, CPU model, and dependency versions.
Measure a CPU-Bound Workload
The clearest starting point is a workload that performs substantial Python computation without spending most of its time waiting for disk, network, or database operations.
The following example calculates the sum of squares over several independent ranges. Each worker performs the same kind of CPU-bound operation, making it suitable for comparing sequential execution with thread-based execution.
import math
import time
from concurrent.futures import ThreadPoolExecutor
def sum_squares(start: int, end: int) -> int:
return sum(i * i for i in range(start, end))
def run_sequential(ranges):
return [sum_squares(start, end) for start, end in ranges]
def run_threaded(ranges, workers: int):
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = [
executor.submit(sum_squares, start, end)
for start, end in ranges
]
return [future.result() for future in futures]
ranges = [
(0, 1_000_000),
(1_000_000, 2_000_000),
(2_000_000, 3_000_000),
(3_000_000, 4_000_000),
]
start = time.perf_counter()
sequential_results = run_sequential(ranges)
sequential_time = time.perf_counter() - start
start = time.perf_counter()
threaded_results = run_threaded(ranges, workers=4)
threaded_time = time.perf_counter() - start
assert sequential_results == threaded_results
print(f"Sequential: {sequential_time:.3f}s")
print(f"Threaded: {threaded_time:.3f}s")
print(f"Speedup: {sequential_time / threaded_time:.2f}x")
The assertion ensures that the two approaches produce identical results. The timing comparison shows whether threads improve this particular workload.
Run the same script with standard CPython and a free-threaded interpreter, ensuring that the latter has the GIL disabled. Use multiple trials and compare median timings rather than relying on a single run.
This is a simple demonstration, not a comprehensive benchmark suite. Python startup, CPU frequency changes, background processes, and task size can all affect results. For a more reliable evaluation, test several workload sizes and use a benchmark harness such as pyperformance.
Why Free-Threading Does Not Guarantee a Speedup
Removing the GIL eliminates one restriction on parallel execution, but it does not eliminate all contention.
Threads can still compete for shared data, locks, memory bandwidth, and CPU resources. If multiple threads repeatedly update the same shared object, synchronization overhead can reduce the benefits of parallelism.
For example, this workload may not scale as expected:
import threading
counter = 0
def increment_many(times: int) -> None:
global counter
for _ in range(times):
counter += 1
threads = [
threading.Thread(target=increment_many, args=(100_000,))
for _ in range(4)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
The code illustrates shared mutable state. Even when individual built-in operations have internal thread-safety protections, a compound operation such as counter += 1 should not be treated as an atomic transaction.
If the application requires an exact counter, protect the update with a threading.Lock or use a suitable concurrent design. Benchmarking an incorrect result is not a meaningful performance comparison.
A better scaling strategy is often to divide work into independent chunks, minimize shared writes, and combine results after the workers finish.
Compare Single-Threaded Performance Too
A free-threaded build may be slower for applications that spend most of their time executing one thread.
The free-threaded interpreter needs additional machinery to support concurrent object access and memory management. Python's free-threading documentation reports that, for Python 3.14, the average overhead on the pyperformance benchmark suite ranged from approximately 1% on macOS ARM64 to 8% on x86-64 Linux.
Those figures describe the documented Python 3.14 results, not a universal performance guarantee for Python 3.15. Hardware, benchmark composition, compiler configuration, and subsequent interpreter improvements can change the outcome.
For Python 3.15, measure the actual interpreter builds you intend to deploy. Include single-thread tests to determine the baseline cost, followed by multithreaded tests to establish whether parallel execution compensates for that cost.
A workload that gains 1.5 times throughput with four threads may be useful, but the result must be weighed against its single-thread performance, memory consumption, and operational complexity.
Check Third-Party Package Compatibility
Native extensions are one of the most important factors when evaluating free-threaded Python.
Packages written entirely in Python may work without modification, but their behavior still needs testing under concurrency. C extensions must account for the free-threaded interpreter's requirements, including how they access Python objects and manage shared state.
Python 3.15 introduces further C API improvements, including the stable ABI for free-threaded builds through the abi3t approach. This helps extension authors distribute compatible binaries, but it does not mean that every existing extension is automatically compatible.
Before migrating an application, check:
Whether each dependency publishes wheels for the free-threaded build.
Whether native extensions explicitly support free-threaded execution.
Whether importing a package enables the GIL.
Whether shared mutable state is protected correctly.
Whether the package's supported Python versions include the target interpreter.
The official Python documentation and community-maintained compatibility trackers are useful starting points. For production systems, verify the exact versions and wheels installed in your environment rather than relying on a package's name alone.
Benchmark I/O-Bound Applications Separately
Free-threading is often discussed in terms of CPU performance, but many Python services spend more time waiting for external systems than executing Python code.
Examples include web applications that wait for database responses, clients that make HTTP requests, and workers that read files from remote storage.
Standard CPython threads can already overlap many blocking I/O operations because the interpreter releases the GIL around common blocking operations. Consequently, free-threading may provide little improvement for an application whose bottleneck is network latency or database throughput.
For these workloads, measure end-to-end throughput, response latency, connection-pool utilization, and downstream service limits.
If the database can handle only a certain number of concurrent queries, allowing more Python threads to execute simultaneously may simply increase contention at the database. Performance improvements depend on the whole system, not just the interpreter.
A Practical Benchmarking Checklist
Use a consistent methodology when comparing the two builds.
Verify the runtime. Record the Python version, build configuration, and actual GIL state.
Use the same application code. Keep task sizes, inputs, dependencies, and output validation consistent.
Separate workload types. Test CPU-bound computation independently from I/O-bound operations.
Vary thread counts. Compare one, two, four, and other sensible worker counts based on the machine's available cores.
Repeat each test. Use warm-up runs and multiple measurements, then compare medians and variability.
Track resource use. Record CPU utilization, peak memory, and context-switching behavior where practical.
Verify correctness. Compare outputs and run race-sensitive tests under concurrent execution.
Check dependency behavior. Confirm that imports do not unexpectedly re-enable the GIL.
Test production-like traffic. Include realistic data sizes, contention, and downstream service limits.
Document the trade-off. Record both the throughput gain and any increase in memory use or operational complexity.
A benchmark is useful only when it measures the workload the application actually needs to run.
When Should You Choose Free-Threaded CPython?
Free-threaded Python is worth evaluating when an application performs substantial CPU-bound work that can be divided into independent tasks, uses multiple threads already, and depends on extensions that support free-threading.
Standard CPython remains a sensible default for applications that do not benefit from parallel Python execution, depend on incompatible extensions, or prioritize the simplest deployment path.
For some workloads, multiprocessing or native code remains a better option. The correct choice depends on the cost of copying data between processes, the amount of shared state, the library ecosystem, and the performance requirements.
Conclusion
Python 3.15's free-threaded build gives developers a way to execute CPU-bound Python code in parallel across multiple threads without relying on the traditional GIL. It is a meaningful option for workloads that can exploit multiple cores, but it is not an automatic performance upgrade.
The standard interpreter can still outperform the free-threaded build for some single-threaded workloads. Native extensions may re-enable the GIL, and shared mutable state still requires deliberate synchronization.
The practical approach is to compare both builds using the same application code, verify that the GIL is actually disabled, measure realistic workloads, and test every important dependency. Adopt free-threading when measured gains justify the additional compatibility and concurrency work—not simply because the interpreter supports it.
Join the conversation! Your thoughts help the community grow.