Tests and CI
There are two ways to catch GIL problems before they ship, and they cover different gaps. The scanner checks every installed package at import time. Running your tests under gilguard catches extensions that only load when a code path runs. Use both.
Run pytest with gilguard loaded
Load gilguard as an early plugin with -p:
pytest -p gilguardpytest imports -p plugins during startup, before it collects your conftest.py files and test modules, so gilguard is watching before any of your code imports anything. To make it the default, add it to your pytest config:
# pyproject.toml
[tool.pytest.ini_options]
addopts = "-p gilguard"A plain import gilguard at the top of conftest.py also works if nothing imported before it touches an extension module. -p is the safer choice because it doesn’t depend on load order.
When a test triggers an import that turns the GIL on, that test fails with the full report:
FAILED test_reports.py::test_export - gilguard.GILEnabledError: Importing 'gilneeder....
The GIL stays on for the rest of the pytest process, and gilguard only reports the first flip. The tests after it still run, but not free-threaded. Treat the first GILEnabledError as the result and fix that before you trust anything else in the run.
Scan in CI
Add the scanner as its own step. It exits 1 when any installed package re-enables the GIL, which fails the job. Here’s a GitHub Actions job that does both checks on a free-threaded interpreter:
jobs:
free-threading:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- name: Create a free-threaded environment
run: |
uv venv -p 3.14t
uv pip install -e . gilguard pytest
- name: Scan installed packages
run: .venv/bin/python -m gilguard --strict
- name: Run tests with gilguard loaded
run: .venv/bin/python -m pytest -p gilguardThe scan prints one line per offender and finishes in seconds for a typical environment, so it’s cheap to run on every push. --strict also fails the job when a package can’t be imported at all, so a broken install can’t pass as “no offenders found”. Drop it if your environment has packages with optional dependencies you don’t install.
A passing job means the imports that ran kept the GIL off. It doesn’t mean the code is thread-safe, or that it gets faster with threads. Those need their own tests.
Standard builds in the same matrix
If your CI matrix also covers standard GIL builds, gilguard will raise there because the GIL is always on. Set GILGUARD_ACTION=off for those jobs and leave it unset for the free-threaded ones. The scanner exits 2 on a standard build, so run it only in the free-threaded job.