ft-readiness
Catch a broken free-threading build before you publish it, not after your users file the bug.
Python 3.14 made free-threading officially supported. If you ship a C extension there are three ways to break it silently — and none of them fail your build:
- Your extension omits the
Py_mod_gilslot. CPython re-enables the GIL the moment someone imports your module on a free-threaded interpreter. Every 3.14t user loses parallelism; the only signal is a warning buried in stderr. - You ship wheels for regular CPython but no
cp314twheel. Free-threaded users fall back to an sdist build, or to the GIL build, with nothing telling you they got the slow path. - Your
Free Threading ::trove classifier says one thing and your wheels say another — you advertise support you didn't ship, or ship support nobody can discover on PyPI.
ft-readiness is a pre-publish gate for all three. Run it in CI after python -m build; it exits non-zero when your artifacts and your claims don't line up.
$ ft-readiness .
✗ FT001 [error] Declares 'Programming Language :: Python :: Free Threading :: 2 - Beta' but ships no free-threaded (cp3XXt) wheel for its compiled extension.
→ Build a cp3XXt wheel (cibuildwheel with CIBW_ENABLE=cpython-freethreading, or CPython 3.14t + your build backend), or drop the Free Threading classifier until you ship one.
! FT004 [warning] Ships abi3 (limited-API) wheels but no cp3XXt wheel. The stable ABI is unsupported on the free-threaded build, so abi3 wheels do NOT cover 3.14t users.
→ Build a separate free-threaded wheel; you can keep abi3 for the GIL build (e.g. py_limited_api=not sysconfig.get_config_var('Py_GIL_DISABLED')).
ft-readiness 0.1.0: 1 error(s), 1 warning(s).
Install
pip install ft-readiness
Python 3.9+. Pure Python, one runtime dependency (packaging).
What it checks
The static checks read your built dist/ and your pyproject.toml (or a wheel's METADATA). They run on any interpreter — you do not need a free-threaded build to run them.
| Code | Level | Fires when |
|---|---|---|
| FT001 | error | A Free Threading :: classifier is declared, the package has a compiled extension, but no cp3XXt wheel ships. You advertise support your artifacts don't deliver. |
| FT002 | warning | A cp3XXt wheel ships but no Free Threading :: classifier is declared. Support is real but undiscoverable on PyPI. |
| FT003 | warning | The package ships compiled-extension wheels but none for free-threaded Python. The silent missing-wheel case. |
| FT004 | warning | Only abi3 (limited-API) wheels ship, no cp3XXt. The stable ABI is unsupported on the free-threaded build, so abi3 does not cover 3.14t. |
| FT005 | warning | The package advertises free-threading but requires-python excludes the versions (3.13+) that have a free-threaded build. |
| FT006 | info | A pure-Python package claims 3 - Stable/4 - Resilient — a reminder to back the claim with thread-safety tests. |
The runtime check is the one that catches a re-enabled GIL, and it needs a free-threaded interpreter (it imports your module and watches the GIL state):
| Code | Level | Fires when |
|---|---|---|
| FT010 | error | Importing a module you named re-enabled the GIL — the Py_mod_gil slot is missing. |
| FT011 | warning | A module named for the runtime check couldn't be imported. |
# on a python3.14t interpreter, in CI:
ft-readiness . --import mypkg._core --import mypkg._fast
On a non-free-threaded interpreter the --import checks are skipped with a note, so the same command is safe everywhere in your matrix.
pytest plugin
If you already run a 3.14t leg in CI, wire the runtime check into pytest instead:
pytest --ft-import mypkg._core --ft-import mypkg._fast
Each module is imported in a fresh subprocess and reported as its own test. On a non-free-threaded build the checks skip.
GitHub Action
- uses: fernforge/ft-readiness@v1
with:
project: . # optional, default '.'
strict: 'false' # optional, treat warnings as errors
The action installs ft-readiness and runs the static checks against your dist/. Run it after your build step. For the runtime GIL check, add a job on actions/setup-python with a 3.14t interpreter and call ft-readiness --import ... there.
Why these three failures and not others
The GIL re-enable prints a warning, so an attentive porter can catch it by reading stderr. The two failures with no signal at all are the missing cp314t wheel and the classifier that doesn't match the wheels — a user just gets the slow path and never tells you. ft-readiness turns all three into a build failure you see before the release goes out.
Options
ft-readiness [PROJECT] [--dist DIR] [--import MODULE ...] [--strict] [--format text|json]
PROJECT— project directory (default.); itsdist/is inspected unless--distis given.--dist DIR— directory of built wheels/sdists.--import MODULE— runtime GIL check for a module (repeatable or comma-separated).--strict— exit 1 on warnings too.--format json— machine-readable output for CI dashboards.
Exit code is 1 if there are any errors (or any warnings under --strict), else 0.
License
MIT
Metadata
Release files for ft-readiness 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ft_readiness-0.1.0.tar.gz | 37.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ft_readiness-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 52.4 kB
Release files / ft_readiness-0.1.0.tar.gz
| Download URL | ft_readiness-0.1.0.tar.gz |
|---|---|
| Size | 37.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cf94389e5c60e1293f9ec1b65b4bf3ebf6dedcd3506fa27773a1fbdb41b7ae03
|
|
BLAKE2b-256 checksum How to use checksums |
5b0d607253be1253e082d28f90e0c3883763035df626f7fd011ce160865f4dea
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.2
|
Release files / ft_readiness-0.1.0-py3-none-any.whl
| Download URL | ft_readiness-0.1.0-py3-none-any.whl |
|---|---|
| Size | 15.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
51e20a01d8d08bde74b0ffd18995ec6e923f43873e6a90dd5b1dc443d2c140ce
|
|
BLAKE2b-256 checksum How to use checksums |
24d9cc40ffb64298510de99a555da0251de6c173befcea5cb5cc488f011476ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.2
|
Security for ft-readiness 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.