Short answer
In traditional CPython the GIL lets only one thread execute bytecode at a time, so CPU-bound work does not benefit from threads. Since Python 3.13 there is an experimental build without the GIL (free-threaded), and it became officially supported in Python 3.14.
Explanation
Threads are useful with I/O because waiting on the network or disk releases the GIL, and C libraries such as NumPy release it during heavy computation. For CPU-bound work in pure Python use multiprocessing, ProcessPoolExecutor or InterpreterPoolExecutor (Python 3.14). The free-threaded build (python3.14t) allows true parallelism between threads, but it comes with a small single-thread slowdown, requires C extensions to be compatible, and makes the need for locks to protect shared state more visible.
Code example
import sys
from concurrent.futures import ProcessPoolExecutor
print(sys._is_gil_enabled()) # Python 3.13+: False In the free-threaded build
def crunch(n: int) -> int:
return sum(i * i for i in range(n))
with ProcessPoolExecutor() as pool: # True parallelism with GIL
results = list(pool.map(crunch, [10**7] * 4))Common mistakes
- Using threads to speed up pure-Python computation on traditional CPython and expecting an improvement.
- Believing the GIL protects code from race conditions; operations such as counter += 1 are not atomic.
Follow-up questions
- How do you choose between multiprocessing, free-threaded Python, and subinterpreters for parallel data processing?
- How do you run a blocking function inside asyncio?