Skip to main content

ftcheck

Alpha (0.1). Read docs/limitations.md before trusting any output.

ftcheck looks for free-threading bugs in Rust/PyO3 extensions: data races, crashes, hangs and panics that appear only when Python threads really run in parallel.

A Rust extension that was correct under the GIL can be silently wrong without it, because the GIL provided mutual exclusion the author never had to think about. The failure mode is data corruption and rare crashes, not a compile error.

Two things this does not claim

Stated up front, because they are the claims a tool in this space is tempted to make.

  • It does not bring free-threading to GitHub Actions. actions/setup-python has installed 3.13t and 3.14t since PR #973.
  • It does not generate a concurrency harness automatically. Constructor synthesis for non-primitive #[pyclass] types is not viable: stress derives constructors that take only primitives, and any other type needs a declared factory.

It also does not distribute a ThreadSanitizer-instrumented CPython image, because one already exists: nascheme/cpython_sanity publishes ghcr.io/nascheme/cpython-tsan, both the py-free-threading guide and the PyO3 guide recommend it, and NumPy's CI already uses it. ftcheck ci consumes that image and adds only the Rust layer.

Install

$ pip install ftcheck        # or: uv tool install ftcheck

Wheels are published for CPython 3.11 and later, including free-threaded 3.14, on Linux (x86-64, aarch64), macOS (x86-64, arm64) and Windows (x86-64). What runs where:

Command Where it runs
ftcheck lint, ftcheck matrix Every platform with a wheel. No Docker, no Rust toolchain.
ftcheck ci, ftcheck stress Linux x86-64 with Docker, inside the ftcheck-tsan image below. They need a ThreadSanitizer-instrumented free-threaded CPython and a matching nightly Rust, which the image provides; the installed package alone does not.

Quick start

Everything runs from one Docker image: the upstream ThreadSanitizer free-threaded CPython, a matching nightly Rust, and ftcheck. Build it once, from this checkout:

$ docker build -f docker/Dockerfile -t ftcheck-tsan .

Then, from your extension's directory, define the one prefix every command uses:

$ mkdir -p ~/.cache/ftcheck-cargo
$ FTCHECK='docker run --rm --security-opt seccomp=unconfined --user '"$(id -u):$(id -g)"' -e HOME=/tmp -v '"$PWD"':/src -v '"$HOME"'/.cache/ftcheck-cargo:/opt/cargo/registry ftcheck-tsan ftcheck'
$ $FTCHECK lint /src      # seconds: a static pre-flight
$ $FTCHECK ci /src        # minutes: your tests, every test body in 8 threads, under TSan
$ $FTCHECK stress /src    # minutes: one shared instance per type, driven from 8 threads

What each part is for:

  • --security-opt seccomp=unconfined — ThreadSanitizer cannot start under the ASLR entropy modern kernels use; this lets it disable ASLR for its own process. Needed by ci and stress; harmless for the rest. See docs/ci.md.
  • --user … -e HOME=/tmp — build output lands in your project owned by you, not root.
  • -v ~/.cache/ftcheck-cargo:/opt/cargo/registry — keeps downloaded crates between runs; without it every run downloads them again.
  • If your crate is not at the repository root, pass its directory (/src/bindings/python) and still mount the whole repository, so path dependencies resolve.

lint and matrix also run without Docker: pip install ftcheck, then ftcheck lint ..

What works today

ftcheck ci — the ThreadSanitizer pipeline

Builds your extension with -Zsanitizer=thread and -Zbuild-std, installs it on a TSan-instrumented free-threaded CPython, and runs your own test suite with every test body in N threads at once, so a module-level instance your tests touch is shared across threads — the situation the GIL used to make safe.

$ $FTCHECK ci /src
built ft001_static_mut.cpython-314t-x86_64-linux-gnu.so with ThreadSanitizer
ran 1 test x 8 threads (pytest exit 0; 0 failed, 0 errors)

1 finding:

  tsan/data-race  src/lib.rs:14:9  [certain]  (reported 2x)
        ThreadSanitizer: data race. Write of size 8 by thread T7 in `bump`;
        Previous write of size 8 by thread T8 in `bump`

Before it builds anything it checks every condition under which a TSan run is silently wrong — mismatched LLVM between Rust and CPython, force_seq_cst_atomics, missing rust-src, a GIL-enabled or uninstrumented interpreter — and refuses with exit 3 rather than reporting a clean run. See docs/ci.md for the full command, GitHub Actions usage, and the host ASLR requirement.

ftcheck stress — one shared instance, many threads

ci finds a race only if your tests happen to share an object between threads, and most suites build a fresh one per test. stress builds one instance of each type and drives every pair of its methods from N threads at once — as many pairs as the time budget allows, and the report says how many that was — under ThreadSanitizer, with a seed that replays the schedule:

$ $FTCHECK stress /src
seed 1234  threads 8  iterations 200  budget 120s
  replay: ftcheck stress --replay 1234 --threads 8

coverage: 1 of 1 callables driven

  ft001_raw_pointer.ft001_raw_pointer.Buffer  [derived: Buffer(1)]  1/1 pairs
    driven      ft001_raw_pointer.ft001_raw_pointer.Buffer.bump  (3200 calls; raised IndexError x1099)

1 finding:

  tsan/data-race  src/lib.rs:35:13  [certain]  (reported 2x)

No test was written for that. Constructors that need more than primitives take a one-line factory in ftcheck.toml; every callable that could not be driven is named, with the reason. fixtures/racy/stress-unshared-state proves the gap: its own tests never share an instance, and the ground truth asserts ci exits 0 and stress exits 1 on it. See docs/stress.md.

ftcheck lint — a fast pre-flight

$ ftcheck lint path/to/your/crate
scanned 1 file, 1 Python entry point, PyO3 0.29.2

1 finding:

  FT001  src/lib.rs:14:9  [certain]
        `bump` reaches the `static mut` `COUNTER` with no synchronisation

The lint is not the product. It is deliberately small, because rustc already rejects most of what a naive rule set would check. See docs/rules.md.

ftcheck matrix prints free-threaded wheel jobs for a release workflow, with every interpreter named explicitly. Check what your maturin version's generate-ci already emits first. See docs/matrix.md.

Exit codes

Code Meaning
0 No confirmed finding on the exercised surface
1 Confirmed finding: a TSan report in your extension, a crash, a hang, or a panic only under concurrency
2 Usage or configuration error
3 Could not run — toolchain or interpreter failure, nothing exercised, or coverage below --min-coverage
4 ci only: your test suite failed under the threaded, instrumented run, and TSan reported no race — often a test that is not thread-safe

3 is the one that matters. ftcheck never claims code is safe, only that nothing was found on the surface it actually exercised — so a run that never happened must not be reportable as a clean one.

What it refuses to assert

  • It does not prove the absence of races. The report reads "no race detected on the exercised surface", never "safe". A test asserts that word never appears.
  • It reports its own coverage — files scanned, entry points seen, and every file it could not parse, named rather than skipped; tests run and threads used. A sanitizer run that executed no test exits 3, not 0.
  • Races inside CPython are reported separately and do not fail your run — including those your extension merely reached, which are labelled as such rather than blamed on it.
  • It does not stop at data races. A crash, a hang, or a Rust panic that happens only under concurrency is a finding too; each is a real failure users would hit, and none of them is a TSan data-race report. What stress cannot yet see — wrong results that return normally, among others — is listed in docs/limitations.md.
  • likely findings are hidden by default and do not affect the exit code unless you ask for them (lint --confidence likely).
  • It does not rate severity or exploitability.
  • It does not replace pytest-run-parallel. That tool runs your existing tests in parallel; it cannot discover issues arising from multithreaded use of data structures defined by the library under test, which is what ftcheck stress is for.
  • The lint's record on public code is published, unflattering or not. Its first run over 23 public PyO3 projects: 3 certain findings, 0 confirmed; FT003 at 13% precision. Both were fixed from that evidence. FT003 now measures 67%, but on the same projects the fixes were learned from, so that is not an independent figure. See docs/limitations.md.

Ground truth

Every rule is backed by fixtures in fixtures/, each carrying an expected.toml that states what a correct tool must say about it — both what the lint must report and whether ThreadSanitizer must see a race. A finding on a clean fixture fails the suite exactly as hard as a miss on a racy one, in both halves.

The most useful pair is racy/ft002-freelist-old-pyo3 and clean/clean-freelist-current-pyo3: the same source apart from the module name, pinned to different PyO3 versions, because whether that code is a bug depends on the framework version and not on the code.

Development

cargo test --workspace     # rust unit tests
pytest tests/              # contract, CLI and ground-truth suites
./scripts/check-publish-guards.sh  # publish guards

docker build -f docker/Dockerfile -t ftcheck-tsan .
FTCHECK_TSAN=1 pytest tests/test_tsan_ground_truth.py   # every fixture under TSan

Licence

MIT OR Apache-2.0.

Metadata

Release files for ftcheck 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ftcheck 0.1.1
File Size Uploaded
ftcheck-0.1.1.tar.gz 95.7 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for ftcheck 0.1.1
File
ftcheck-0.1.1-cp314-cp314t-win_amd64.whl CPython 3.14 CPython 3.14 free-threading Windows x86-64 Details
ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_x86_64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ x86-64 Details
ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_aarch64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ ARM64 Details
ftcheck-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 Details
ftcheck-0.1.1-cp314-cp314t-macosx_10_12_x86_64.whl CPython 3.14 CPython 3.14 free-threading macOS 10.12+ x86-64 Details
ftcheck-0.1.1-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
ftcheck-0.1.1-cp311-abi3-manylinux_2_28_x86_64.whl CPython 3.11 abi3 Linux glibc 2.28+ x86-64 Details
ftcheck-0.1.1-cp311-abi3-manylinux_2_28_aarch64.whl CPython 3.11 abi3 Linux glibc 2.28+ ARM64 Details
ftcheck-0.1.1-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
ftcheck-0.1.1-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 13.3 MB

Release files / ftcheck-0.1.1.tar.gz

Download URL ftcheck-0.1.1.tar.gz
Size 95.7 kB
Tags Source
SHA-256 checksum
How to use checksums
cbb1ce7a1e1a83952ebc46df18470eb18d238c49aaa041c08d97007b5769f0f7
BLAKE2b-256 checksum
How to use checksums
7ec2bf31cfb40d7ffad3c4f1d083604bbba99dbe572bc7bdfb60d7961daa6f93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp314-cp314t-win_amd64.whl

Download URL ftcheck-0.1.1-cp314-cp314t-win_amd64.whl
Size 1.3 MB
Tags CPython 3.14 CPython 3.14 free-threading Windows x86-64
SHA-256 checksum
How to use checksums
eba421becb8f0b47a630c9ac3aada5097ab53a5a0868db71eb42ee5eb3b2bec9
BLAKE2b-256 checksum
How to use checksums
d25690f2243e6f73e91d78e711ca49c94cd2f6f35b9feb0d12bb4ede15703492
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_x86_64.whl

Download URL ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_x86_64.whl
Size 1.4 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ x86-64
SHA-256 checksum
How to use checksums
19488bb40f6615a43422dcddd0197817fd1625971fb90b39fce34cae4ef97c6c
BLAKE2b-256 checksum
How to use checksums
2cd389a2ea56f42ea0e4bcce21f5c8a08b77e6e9fc718d269ec03580e73cabb3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_aarch64.whl

Download URL ftcheck-0.1.1-cp314-cp314t-manylinux_2_28_aarch64.whl
Size 1.3 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ ARM64
SHA-256 checksum
How to use checksums
8d8d87b69b6dc1e35b4f30d30b2c60664963e1bac509ee566c30abd8448f9e0f
BLAKE2b-256 checksum
How to use checksums
743581156742d107cf802d6a48d7ad6a4436a95d67c78dc57eea537e8ee0b723
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl

Download URL ftcheck-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl
Size 1.2 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
38a5664345c090a9ec8bb11999ede7cb02bd48d8c43821ffc6adf6b79e1ba7b1
BLAKE2b-256 checksum
How to use checksums
95195a36aa7daaa284f543ec6fe6ab2dc26f8e9a26ad06def260f01c43b733ba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp314-cp314t-macosx_10_12_x86_64.whl

Download URL ftcheck-0.1.1-cp314-cp314t-macosx_10_12_x86_64.whl
Size 1.3 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
bca45eb447d31c477dfecda733bcdfdef190450d56d5c20ce8302106666fc7e8
BLAKE2b-256 checksum
How to use checksums
81b206c2de4266916a52753139912d921c860a15323f5df05b6bf9b37f6e455e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp311-abi3-win_amd64.whl

Download URL ftcheck-0.1.1-cp311-abi3-win_amd64.whl
Size 1.3 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
7d153837293d585af42e38be521a2918863198654d760c735657ddc7fc76824a
BLAKE2b-256 checksum
How to use checksums
e529a98f612b4a146a915d450895b9bb71f8aec12c4f03a6955e5e1f4e57c754
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp311-abi3-manylinux_2_28_x86_64.whl

Download URL ftcheck-0.1.1-cp311-abi3-manylinux_2_28_x86_64.whl
Size 1.4 MB
Tags CPython 3.11 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
dab831595f281504027c78e5811979412c4c68e1796f24e0470a3817439ee7b8
BLAKE2b-256 checksum
How to use checksums
16e1c3abe376967589f8c2596148dc74712dd71f9f322bcae46089fa3cdc61f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp311-abi3-manylinux_2_28_aarch64.whl

Download URL ftcheck-0.1.1-cp311-abi3-manylinux_2_28_aarch64.whl
Size 1.3 MB
Tags CPython 3.11 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
0315def01f1025a52ed1d1d774f7cb3f88b5604b070efe7be8958f5fd7285d55
BLAKE2b-256 checksum
How to use checksums
559f5a9030f41e780fa92fb91c48f115e8ebf0cbb42f6428132c09ef7f6b0307
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp311-abi3-macosx_11_0_arm64.whl

Download URL ftcheck-0.1.1-cp311-abi3-macosx_11_0_arm64.whl
Size 1.2 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
f83b0d5c4f55ce7e6f1fa3337d4bce486b9af3066263706f59944839e5bf214c
BLAKE2b-256 checksum
How to use checksums
1cd298302b76ad3b58071701dcbab32e244702770e96bc42e3df658d76b89124
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / ftcheck-0.1.1-cp311-abi3-macosx_10_12_x86_64.whl

Download URL ftcheck-0.1.1-cp311-abi3-macosx_10_12_x86_64.whl
Size 1.3 MB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
7c9f5c9adee9eb0d2139fee10476d1ae9d9153378aa5d1440d85a4d64c3069f6
BLAKE2b-256 checksum
How to use checksums
c3d3e37a963d45dccf8c367d1f973ea7df749a8b9aa90db8d65d02169f4a87a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

11 release files

0.1.0

11 release files

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