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.

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. 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.1.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.1.0
File Size Uploaded Provenance Actions
gilguard-0.1.0.tar.gz 11.6 kB Without provenance Details for gilguard-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for gilguard 0.1.0
File Interpreter ABI Platform Actions
gilguard-0.1.0-py3-none-any.whl Python 3 none any Details for gilguard-0.1.0-py3-none-any.whl

Total release size: 24.4 kB

Release files / gilguard-0.1.0.tar.gz

Download URL gilguard-0.1.0.tar.gz
Size 11.6 kB
Tags Source
SHA-256 checksum
How to use checksums
d7aed36bf0992a9a888854d4f45de6f8553dad697696198f1f17447e05f44a6a
BLAKE2b-256 checksum
How to use checksums
aca32b8ad30d088ca639845c125eca92bf0f76eb1bdd810b0e198a598e590485
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.1.0-py3-none-any.whl

Download URL gilguard-0.1.0-py3-none-any.whl
Size 12.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4d86316d777fd3de0c60b0bb2b267c753b75a48725df7c56e27451b8f4ed2057
BLAKE2b-256 checksum
How to use checksums
517815335f290dbda4f8fef141f3d9dfcd5c50a833013a96c8a266fe60f163d8
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.1.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.1.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