Skip to main content

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:

  1. Your extension omits the Py_mod_gil slot. 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.
  2. You ship wheels for regular CPython but no cp314t wheel. Free-threaded users fall back to an sdist build, or to the GIL build, with nothing telling you they got the slow path.
  3. 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 .); its dist/ is inspected unless --dist is 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)

Source distribution for ft-readiness 0.1.0
File Size Uploaded Provenance
ft_readiness-0.1.0.tar.gz 37.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for ft-readiness 0.1.0
File Interpreter ABI Platform
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.

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