Python 3.13 without GIL: Real-World Threading Finally Works
Python 3.13 introduces the most significant concurrency improvement in CPython's history: the ability to disable the Global Interpreter Lock (GIL). Thanks to PEP 703, Python can now execute native threads in parallel — not just in theory, but in practice.
This post shares hands-on results after compiling Python 3.13 with --disable-gil
, running CPU-heavy multithreading tasks, and toggling the GIL on and off at runtime. The verdict? Python threads finally scale — and in some cases, outperform multiprocessing — with no code changes.
How to Install Python 3.13 Without the GIL¶
To run GIL-free Python, you’ll need to compile it from source.
Requirements (Ubuntu)¶
sudo apt update
sudo apt install -y build-essential zlib1g-dev
Build and Install¶
wget https://www.python.org/ftp/python/3.13.0/Python-3.13.0.tgz
tar -xf Python-3.13.0.tgz
cd Python-3.13.0
./configure --disable-gil --prefix=$HOME/python3.13
make
make install
Add to your shell path or run directly:
$HOME/python3.13/bin/python3.13 -X gil=0
Benchmarking: Threads vs Processes vs Serial Execution¶
To truly evaluate the GIL, I wrote the following script to simulate a real CPU workload: calculating factorials using threads, processes, and single-threaded code.
import sys
import sysconfig
import math
import time
from threading import Thread
from multiprocessing import Process
def time_taken(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"Function '{func.__name__}' took {end - start:.4f} seconds to execute.")
return result
return wrapper
def compute_intensive_task(num):
return math.factorial(num)
@time_taken
def single_threaded_task(nums):
for num in nums:
compute_intensive_task(num)
@time_taken
def multi_threaded_task(nums):
threads = []
for num in nums:
t = Thread(target=compute_intensive_task, args=(num,))
threads.append(t)
t.start()
for t in threads:
t.join()
@time_taken
def multi_processing_task(nums):
processes = []
for num in nums:
p = Process(target=compute_intensive_task, args=(num,))
processes.append(p)
p.start()
for p in processes:
p.join()
def main():
print(f"Python Version: {sys.version}")
gil_enabled = sys._is_gil_enabled()
print("GIL is currently", "active" if gil_enabled else "disabled")
nums = [300001] * 6
single_threaded_task(nums)
multi_threaded_task(nums)
multi_processing_task(nums)
if __name__ == "__main__":
main()
Results: GIL OFF vs ON¶
GIL Disabled¶
PYTHON_GIL=0 ./python3.13/bin/python3 script1.py
Python Version: 3.13.0 experimental free-threading build
GIL is currently disabled
Function 'single_threaded_task' took 4.8051 seconds to execute.
Function 'multi_threaded_task' took 2.3236 seconds to execute.
Function 'multi_processing_task' took 2.6521 seconds to execute.
GIL Enabled¶
PYTHON_GIL=1 ./python3.13/bin/python3 script1.py
Python Version: 3.13.0 experimental free-threading build
GIL is currently active
Function 'single_threaded_task' took 4.8303 seconds to execute.
Function 'multi_threaded_task' took 4.5740 seconds to execute.
Function 'multi_processing_task' took 2.5155 seconds to execute.
Interpretation¶
Mode | GIL Enabled | GIL Disabled | Performance Impact |
---|---|---|---|
Single-threaded | 4.83s | 4.80s | ~identical |
Multi-threaded | 4.57s | 2.32s | >2× faster without GIL |
Multi-processing | 2.51s | 2.65s | Comparable |
Observations:¶
- Without the GIL, Python threads scale across cores — no workarounds, no hacks.
- GIL-free threading outperformed multiprocessing, without the complexity of IPC.
- The single-thread baseline remains unaffected — the GIL toggle doesn’t impact serial code paths.
Why This Matters¶
Historically, Python's GIL has made multithreading useless for CPU-bound code. This forced developers to:
- Use
multiprocessing
, which incurs overhead - Offload to C or Cython
- Rewrite in other languages (Go, Rust, C++)
With Python 3.13’s --disable-gil
build, those workarounds are no longer necessary. Multithreaded CPU-bound Python code is finally viable.
Considerations¶
- GIL-disabled builds are not ABI compatible — C extensions must be rebuilt.
- Some overhead exists in managing locks around containers, especially in pure Python workloads.
- This is still marked as experimental in Python 3.13. Future versions will likely optimize further.
Final Thoughts¶
Python 3.13 with --disable-gil
is not just a technical milestone — it's a practical, user-facing shift in what Python can do. For the first time, multithreaded Python isn't a theoretical dream. It's fast, predictable, and works without changing your code.
If your workloads involve concurrency, especially CPU-heavy tasks, this feature is worth adopting early.
FAQs
What is the significance of Python 3.13’s --disable-gil feature?
Python 3.13 introduces a GIL-free build option via --disable-gil
, allowing native threads to execute in true parallel. This eliminates the long-standing Global Interpreter Lock bottleneck for CPU-bound multithreaded workloads.
How does performance compare with and without the GIL?
When the GIL is disabled, multithreaded CPU-bound code scales across cores, outperforming both multiprocessing (due to lower overhead) and traditional GIL-constrained threads. Serial code performance remains unaffected.
Do I need to change my code to benefit from GIL-free Python?
No. Python 3.13’s GIL-free build works without any code changes. Existing multithreaded programs can benefit immediately if the GIL is disabled at compile time and runtime.
Are there any compatibility concerns with --disable-gil?
Yes. GIL-disabled builds are not ABI-compatible with standard CPython builds. C extensions must be recompiled, and some thread-safety assumptions in third-party packages may need to be reviewed.
Is Python 3.13’s GIL-free mode production-ready?
It is currently experimental. While real-world performance improvements are promising, users should expect ongoing changes and optimizations in future versions before relying on it in critical production environments.