Skip to main content

gilguard: Catch the import. Name the culprit. Keep threads free.

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_path finder wraps every loader and checks sys._is_gil_enabled() before and after the module is created and executed.
  • Imports that bypass sys.meta_path, such as importlib.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=1 or -X gil=1 forced 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)

Source distribution for gilguard 0.2.0
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)

Table of built distributions (wheels) for gilguard 0.2.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page