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.

Release files for ftcheck 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 ftcheck 0.1.0
File Size Uploaded
ftcheck-0.1.0.tar.gz 86.1 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for ftcheck 0.1.0
File
ftcheck-0.1.0-cp314-cp314t-win_amd64.whl CPython 3.14 CPython 3.14 free-threading Windows x86-64 Details
ftcheck-0.1.0-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.0-cp314-cp314t-manylinux_2_28_aarch64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.28+ ARM64 Details
ftcheck-0.1.0-cp314-cp314t-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 Details
ftcheck-0.1.0-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.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
ftcheck-0.1.0-cp311-abi3-manylinux_2_28_x86_64.whl CPython 3.11 abi3 Linux glibc 2.28+ x86-64 Details
ftcheck-0.1.0-cp311-abi3-manylinux_2_28_aarch64.whl CPython 3.11 abi3 Linux glibc 2.28+ ARM64 Details
ftcheck-0.1.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
ftcheck-0.1.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 13.2 MB

Release files / ftcheck-0.1.0.tar.gz

Download URL ftcheck-0.1.0.tar.gz
Size 86.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3730a5d0f2b02bbc7bca2f42955035b6a6d4549a872f7c471a2648797cb90c17
BLAKE2b-256 checksum
How to use checksums
08741a0a801f06eb08ab5319321c4cce5e331a4e03a934397c9a02ba6eca943b
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
046e6ae0b5e29fb9e09d209f49f944a48265aba41ace16485f2b92cdeb779fcf
BLAKE2b-256 checksum
How to use checksums
55070eafa6ac711d6714520b1dc4891beddbcadb50b26c5b336da0fcc563603a
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
166bd0a83b18aa3643695cb666e62ae80dba6e00eab3742c51278c84da7e9df5
BLAKE2b-256 checksum
How to use checksums
5767b85efc850716c12377d274341b0c8a3501187af13968d630975b24d84887
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
6b795e39b2b94d8848560f8a18cce52a4c71f4ae2fe8ef55f0285d03c1a096ab
BLAKE2b-256 checksum
How to use checksums
ae48d1176f0cfb022f53540bfed2871b23142a3ef2e649b7516d478702d7185d
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
c4d0d6c0c5aaced00378542d736c2cd884fad65a5b1c5561d09642c4eb77b46f
BLAKE2b-256 checksum
How to use checksums
9d7fca4f3bb780b0da1d993793b6833d89f84443ab2e13f75d6fb9e4b25c185a
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
1b54d265ef3df31806f2c0ecfbc33b9a9bf56896070e2220cd60836ba5efae23
BLAKE2b-256 checksum
How to use checksums
e46426275f71f46eff7a0e4de356e2233e087ea84be1883bc8b4e0d6222b9f92
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-cp311-abi3-win_amd64.whl
Size 1.3 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
01356f0f6b0c853e3502ea26d8cf1348553fb3b00303d4074bb35accb183e742
BLAKE2b-256 checksum
How to use checksums
2e43705f730ccb948b22d2683ac170774af5af2c908c7f626fcd32654db372aa
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
5a2ceca5f704835f0fe137f6327a54aca3d6fc450417e40c98247d1137c68be7
BLAKE2b-256 checksum
How to use checksums
0a748f16beb492654152d53282e211591e2032116083d297e9b1d835283ee864
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
3ed31b5b1c11f6d8e174a70aee00268b7dc9697b5a441875dd592f9b4ddb5abb
BLAKE2b-256 checksum
How to use checksums
352ce964f43a59930d84e16d52b211dff30556f7712b49a8e3d81451b2acb020
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
d4dff03a8329f18bb46d9d65b00893d465cce339dc72cfb2fb9a3a196e585079
BLAKE2b-256 checksum
How to use checksums
0122daf9cafe857df281204b1b4ace80889def57fa9c3dee8e7a89ed18f82b3a
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 25, 2026.

Transparency log

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

Download URL ftcheck-0.1.0-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
bcbb8de7020da6326df3d856f9b0c1b56f52a31c99021bcc79b9edd76bdc8ca4
BLAKE2b-256 checksum
How to use checksums
5e3809576383c44e26f94f74b03933371230462634c07268626f7c0c2b100868
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 25, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

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