gilguard
Raise a detailed error the moment an import re-enables the GIL on free-threaded Python.
Full documentation: mkennedy.codes/docs/gilguard
On a free-threaded build (3.13t, 3.14t, 3.15t), importing an extension module that doesn't declare free-threading support quietly turns the GIL back on for the rest of the process. CPython prints one RuntimeWarning and moves on. Once it's on, it can't be turned off.
gilguard turns that into an exception that names the module, the package it came from, and the line of your code that pulled it in.
Installation
uv pip install gilguard
Usage
Make it the first import in your program:
import gilguard # must be first
import fastapi
import pandas # <- raises here if anything in pandas' import tree needs the GIL
gilguard.GILEnabledError: Importing 'gilneeder._speedups' re-enabled the GIL.
Module: gilneeder._speedups
File: .../site-packages/gilneeder/_speedups.cpython-314t-x86_64-linux-gnu.so
Distribution: gilneeder 1.2.0
Import chain: gilneeder -> gilneeder._speedups
Your code: /home/me/app.py:5
Imported by: .../site-packages/gilneeder/__init__.py:1
This extension module does not declare free-threading support (Py_mod_gil), so CPython turned the GIL back on for the rest of the process. It cannot be turned off again.
Options:
- Upgrade gilneeder to a release that ships free-threaded (cp314t) wheels.
- Drop or replace gilneeder.
- If you have verified it is thread-safe, run with PYTHON_GIL=0 to keep the GIL off (at your own risk).
- Run `python -m gilguard` to list every installed package that does this.
The report is also available as data on exc.report (GILReport, with .as_dict()).
What it catches
- Any import that flips the GIL. A
sys.meta_pathfinder wraps every loader and checkssys._is_gil_enabled()before and after the module is created and executed. - Imports that bypass
sys.meta_path, such asimportlib.util.module_from_spec. gilguard also hooks CPython's own warning to catch these. - The GIL already being on when gilguard is imported. The report says whether
PYTHON_GIL=1or-X gil=1forced it, or lists the third-party extension modules already loaded as suspects. - A standard (GIL) build. gilguard raises there, since the GIL is always on.
GILEnabledError derives from BaseException, not Exception. The GIL can't be turned back off, so the common optional-import pattern (try: import x / except ImportError:) must not be able to swallow it and keep running with the GIL on.
Imports in worker threads
The error is raised in the thread that ran the import, like any other exception. If that is a worker thread, the thread dies and prints the report, but the main thread keeps running with the GIL on and the process can exit 0. concurrent.futures holds the error until you call .result(). gilguard does not kill the process for you. If you want a flip in any thread to stop the program, install a threading.excepthook:
import os, threading
import gilguard
_prev_hook = threading.excepthook
def _fail_fast(args):
_prev_hook(args) # print the report as usual
if issubclass(args.exc_type, gilguard.GILEnabledError):
os._exit(1)
threading.excepthook = _fail_fast
What passing means
A clean run means none of the imports that actually ran turned the GIL back on. It does not mean your code or your dependencies are thread-safe, and it does not mean your program gets faster with more threads. Extensions loaded lazily on a code path you didn't exercise aren't checked either.
Configuration
GILGUARD_ACTION environment variable:
| Value | Behavior |
|---|---|
raise (default) |
Raise GILEnabledError |
warn |
Emit GILGuardWarning with the same report and keep running |
off |
Do nothing (e.g. to run the same code on a standard build) |
Or call gilguard.install(action=...) / gilguard.uninstall() directly.
PYTHON_GIL=0 keeps the GIL off no matter what is imported, so gilguard never fires under it.
Finding every offender
A process can only detect the first module that flips the GIL. After that it's already on. To audit a whole environment, python -m gilguard imports each installed top-level package in its own fresh interpreter:
$ 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.
Pass module names to check specific ones, --json for machine-readable output. Exits 1 if any offender is found, so it works as a CI gate. Modules that fail to import on their own are listed as ERROR but don't fail the run, since they couldn't be checked. Add --strict to fail on those too. Extensions a package loads lazily on first use won't show up in the scan. Run your test suite with import gilguard in conftest.py to catch those.
Development
uv venv -p 3.14t
uv pip install -e . pytest ./tests/fixtures/gilneeder ./tests/fixtures/ftsafe
python -m pytest
The fixtures are two tiny C extensions: one without Py_mod_gil (re-enables the GIL) and one that declares Py_MOD_GIL_NOT_USED.
The docs site is built with Great Docs (needs Quarto). The source is great-docs.yml, index.qmd, and user_guide/. Build it with python scripts/build_docs.py, which writes the finished site to site/ and points its images at the hosted copies. Publish the contents of site/ to https://mkennedy.codes/docs/gilguard/. Use uvx great-docs preview for a local preview while writing.
Metadata
Release files for gilguard 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| gilguard-0.2.0.tar.gz | 12.2 kB | Without provenance | Details for gilguard-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| gilguard-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for gilguard-0.2.0-py3-none-any.whl |
Total release size: 25.8 kB
Release files / gilguard-0.2.0.tar.gz
| Download URL | gilguard-0.2.0.tar.gz |
|---|---|
| Size | 12.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eefdcd41e89d224b51ec05be9a55559e8b05f4b0e189f3dc29580509bcb306d2
|
|
BLAKE2b-256 checksum How to use checksums |
a263e164067a2d1d69e61e75aaf3b549e3c83202d9836b35fab41645905a5209
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / gilguard-0.2.0-py3-none-any.whl
| Download URL | gilguard-0.2.0-py3-none-any.whl |
|---|---|
| Size | 13.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4c71e87209a28f2daa6265fb5ad99003df25f9b0f7fe1036a22d908fc09341b7
|
|
BLAKE2b-256 checksum How to use checksums |
18fba76ae5208f2c8f354215d92619d6fe9113507d13acde022c626077f296f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for gilguard 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.