Just a loop over random:
def pi(trials):
ran = random.Random()
inside = 0
for _ in range(trials):
x = ran.uniform(-1, 1)
y = ran.uniform(-1, 1)
if x * x + y * y <= 1:
inside += 1
return 4.0 * inside / trials
On python3.14t:
1 threads: 2.22 s
2 threads: 1.13 s
4 threads: 0.60 s
8 threads: 0.43 s
Drop the t and the times stay flat — that's the GIL serializing threads.
An extension has to declare it doesn't need the GIL.
Import any extension that hasn't opted in, and CPython silently switches the
GIL back on (with a warning) to keep that code safe.
Every extension in the process must be free-threading-aware,
or nobody gets the speedup.
The compute moves to C++ — the opt-in is the interesting one line.
In the module macro:
PYBIND11_MODULE(_core, m,
py::mod_gil_not_used()) {
m.def("pi", &pi);
}
In CMake — code unchanged:
nanobind_add_module(
_core FREE_THREADED
freecomputepi/_core.cpp)
Same Monte Carlo loop, same near-linear scaling — an order of magnitude
faster per thread (0.26 s → 0.07 s, 1→8 threads).
No wrapper writes anything for you. In Python 3.15, the module export is a
slot array returned from a hook (PEP 793):
static PySlot slots[] = {
PySlot_STATIC_DATA(Py_mod_name, (void *)"_core"),
PySlot_STATIC_DATA(Py_mod_methods, methods),
PySlot_DATA(Py_mod_abi, &abi_info),
PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), // ← the opt-in
PySlot_END,
};
PyMODEXPORT_FUNC PyModExport__core(void) { return slots; }
Bonus: 3.15's stable ABI covers free-threaded builds
(PEP 803) — one _core.abi3t.so keeps
working on future t Pythons.
Declaring the module GIL-free tells CPython "I have no unguarded shared
state."
pi uses only local variables → safestd::mutex, atomics, or nanobind's nb::ft_mutexEvery opt-in above — macro, CMake flag, or slot — makes the same promise.
Free-threaded wheels get their own ABI tag: cp314t, separate from cp314.
As of 3.14 it's no longer experimental — no enable needed:
[tool.cibuildwheel]
build = "cp314*"
cp314* matches both cp314 and cp314t, so each job emits both wheels.
Free-threaded users automatically get the t wheel.
py::mod_gil_not_used() · nanobind: FREE_THREADED · C API:Py_mod_gil slot (3.15 export, stable-ABI abi3t capable)cp314* for both regular and cp314t wheels