Free-threaded Python 3.14 scales a pure-Python loop 4ร on 8 threads
I split one pure-Python CPU loop across 1, 2, 4 and 8 threads, once on the normal CPython 3.14.8 and once on its free-threaded build (3.14t, GIL off). The normal build took 1.2 s no matter how many threads shared the work. The free-threaded build went from 1.16 s on one thread to 0.28 s on eight, a 4.1ร speedup. It did not scale linearly, and it did not get slower on a single thread either.
The test
Each thread runs for i in range(n): s += i*i % 7 on its own local variable, and the total work is fixed at 40 million iterations, divided evenly among the threads. No shared state, no locks. I took the best of three runs per thread count, in a Linux aarch64 container with 10 cores, with nothing else running.
| threads | GIL build (s) | free-threaded (s) | speedup vs 1 thread |
|---|---|---|---|
| 1 | 1.219 | 1.158 | 1.0ร |
| 2 | 1.228 | 0.619 | 1.9ร |
| 4 | 1.233 | 0.396 | 2.9ร |
| 8 | 1.224 | 0.280 | 4.1ร |
What stands out
The GIL column is flat to within 1%: extra threads cost nothing and gain nothing, which is the textbook behavior. The free-threaded column scales well to two threads (1.9ร) and then falls off. Eight threads reach 4.1ร, not 8ร. I didn't dig into why. Some of it is probably that the container's 10 cores include efficiency cores, and some may be the free-threaded interpreter's per-object reference counting, but I didn't test either guess.
The single-thread number is the one I'd have worried about. Free-threaded builds were once reported as noticeably slower on one thread. Here the free-threaded build was 5% faster, which I'd treat as a tie. This loop touches only small integers and one local variable, so it says little about code that hammers shared dicts or lists.
Takeaway
If you have CPU-bound pure-Python work that you've been pushing into multiprocessing only to dodge the GIL, a plain threading version on 3.14t is worth trying. It's about 30 MB to install with uv python install 3.14t. Check that your C-extension dependencies have free-threaded wheels first, because importing one that doesn't can turn the GIL back on for the whole process. You can see which state you're in with sys._is_gil_enabled().
Method: CPython 3.14.8 and 3.14.8+freethreaded from the same uv release, one synthetic integer loop, one machine, best of three. Memory-bound code, shared mutable state and real libraries could all scale differently.