Skip to main content

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 cp314t tag 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.

NoteRe-raise if you catch it

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.