Getting started
The problem gilguard solves
Free-threaded Python (the t builds: 3.13t, 3.14t, 3.15t) runs without the global interpreter lock, so threads can run Python code in parallel. That only holds while every extension module you load agrees to it. C extensions declare that with the Py_mod_gil slot. When CPython loads one that hasn’t, it plays safe and switches the GIL back on for the whole process.
It tells you with a single line:
RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module
'gilneeder._speedups', which has not declared that it can run safely without the GIL.
That’s easy to miss in a busy log, and it only names the extension module, not the package you imported or the line that imported it. Once the GIL is on, nothing can turn it off again. The program keeps working, just without the parallelism you switched builds for.
gilguard makes that moment loud and specific.
Install
uv pip install gilguardPlain pip install gilguard works too. gilguard has no dependencies and needs Python 3.13 or newer. To be useful it has to run on a free-threaded interpreter. If you don’t have one yet:
uv python install 3.14t
uv venv -p 3.14tMake it the first import
import gilguard # must be first
import fastapi
import pandasImporting gilguard installs the guard. You don’t have to call anything. From then on, any import that turns the GIL on raises GILEnabledError at that import statement, with a report of what happened.
It has to come first because gilguard can only watch imports that happen after it. If something earlier has already turned the GIL on, gilguard notices when it’s imported and raises straight away, listing the extension modules already loaded. One of them is the culprit.
Put it at the top of whatever module starts your process: main.py, the module your ASGI server loads, manage.py, or __main__.py. For tests, see Tests and CI. There’s a better spot than conftest.py.
Try it
The fastest way to see what’s in your environment is the scanner:
python -m gilguardGIL gilneeder via gilneeder._speedups [gilneeder 1.2.0]
8 checked: 7 ok, 1 re-enable the GIL, 0 failed to import.
Each package gets imported in its own interpreter, so one offender doesn’t hide the rest. Auditing an environment covers the options.
On a standard build
On a regular GIL build there’s nothing to guard, and gilguard says so by raising:
gilguard.GILEnabledError: This is not a free-threaded Python build, so the GIL is always on.
If the same code has to run on both kinds of build, set GILGUARD_ACTION=off wherever it runs with the GIL. Configuration has the details.