Skip to main content

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 gilguard

Plain 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.14t

Make it the first import

import gilguard  # must be first

import fastapi
import pandas

Importing 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.

TipWhere “first” is in your app

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 gilguard
GIL    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.