Reading the report
Every GILEnabledError carries a GILReport. The exception message is the report rendered as text. exc.report is the same thing as data.
The import report
This is the one you’ll see most. An import just turned the GIL on:
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
Here’s what each line tells you:
- Module
- The extension module that doesn’t declare free-threading support. This is the thing that actually flipped the GIL.
- File
-
Where that module lives. The filename shows the ABI it was built for. A
cp314ttag means a free-threaded wheel was installed and the module just doesn’t declare support yet. - Distribution
- The installed package that shipped the module, and its version. This is what you’d upgrade, pin or replace.
- Import chain
- Every module on the way down, starting with the one your code imported. Useful when the offender is a dependency of a dependency.
- Your code
- The import statement where the chain started, usually in your own code.
- Imported by
- The statement that loaded the offending module, usually inside the package. It’s left out when it’s the same line as “Your code”.
If gilguard can’t match the module to an installed distribution (it’s on sys.path from a source checkout, say), the Distribution line says so.
After the details, the report lists your options: upgrade to a release with free-threaded wheels, drop or replace the package, or, if you’ve confirmed it’s thread-safe, run with PYTHON_GIL=0 to keep the GIL off anyway.
Two other reasons
The GIL can also be on before gilguard gets a chance to watch.
It was already on when gilguard was imported. If the GIL was forced on, the report says so:
gilguard.GILEnabledError: The GIL was already enabled before gilguard was imported.
Cause: the GIL was forced on with PYTHON_GIL=1 or -X gil=1.
Make `import gilguard` the very first import in your program.
Otherwise gilguard lists the third-party extension modules that were already loaded. One of them did it:
gilguard.GILEnabledError: The GIL was already enabled before gilguard was imported.
Third-party extension modules already loaded (one of these turned it on):
- gilneeder._speedups
Make `import gilguard` the very first import in your program.
If that list is empty, something ran before your code did: a sitecustomize module or a .pth file are the usual suspects.
It’s a standard build. The GIL is always on, so gilguard raises with the interpreter path and a pointer to GILGUARD_ACTION=off.
Using the report in code
The same fields are on exc.report, and as_dict() gives you something you can serialize:
import json
import gilguard
try:
import gilneeder
except gilguard.GILEnabledError as e:
print(json.dumps(e.report.as_dict(), indent=2))
raise{
"reason": "import",
"module": "gilneeder._speedups",
"origin": ".../site-packages/gilneeder/_speedups.cpython-315t-darwin.so",
"distribution": "gilneeder",
"version": "1.2.0",
"chain": ["gilneeder", "gilneeder._speedups"],
"imported_by": ".../site-packages/gilneeder/__init__.py:1",
"your_code": "app.py:4",
"gil_flag": null,
"suspects": []
}reason is "import", "already-enabled" or "gil-build", and tells you which of the other fields are filled in. The GILReport reference describes each field.
Catching GILEnabledError doesn’t put things back. The GIL is on for the rest of the process either way. Catch it to log or report, then let it go.
Only the first one
gilguard reports the first import that turns the GIL on and stays quiet after that. Once it’s on, later imports can’t turn it on again, so there’s nothing more to catch in that process. To find every offender, use python -m gilguard, which gives each package its own interpreter.