Skip to main content

Python 3.15 shipped on 9 October 2026. Buried in the release notes, under “Release changes”, is one line that changes the nature of a question most teams have been deferring for two years: the official macOS binaries now install free-threading support by default. Not as a separate installer. Not behind a checkbox you have to go looking for. By default.

Free-threaded Python has been officially supported since 3.14, when PEP 779 moved it out of the explicitly experimental phase. “Officially supported” has meant, in practice, “go and get it if you want it”. From 3.15, on the most common developer laptop in the industry, it is simply there, one interpreter selection away, and somebody on your team is going to select it.

So we measured what happens when they do. On 20 October 2026 we pulled the 500 most-downloaded packages on PyPI and checked, for the current release of each, two separate things: whether pip can install it on a free-threaded interpreter, and whether the maintainer has said anything about whether it is safe to use threads with it.

Those turn out to be almost entirely different populations.

TL;DR

  • Of the top 500 PyPI packages, 108 ship compiled wheels, 391 are pure Python, and one (pyspark) ships no wheel at all.
  • 73 of those 108 ship a cp314t wheel, so they install cleanly on free-threaded Python 3.14. 20 ship a GIL wheel but no free-threaded one, and pip quietly falls back to building from source.
  • psycopg-binary has published no source distribution in any of its last five releases. On a free-threaded interpreter there is nothing to fall back to: the install simply fails.
  • The trove classifier that declares free-threading support is used by 26 of 500 packages. Of the 73 that ship a free-threaded wheel, only 14 declare a thread-safety level. The two signals answer different questions and almost nobody publishes both.
  • 31 of the 73 ship free-threaded wheels for fewer platforms than their GIL wheels. Nine of those drop a desktop or server target, so the dependency installs on your Linux CI and fails on a colleague’s Mac.

What we measured, and how

The package list is the top-pypi-packages snapshot dated 1 October 2026, which ranks projects by September 2026 download counts drawn from PyPI’s public ClickHouse dataset. We took the first 500 rows.

For each project we fetched https://pypi.org/pypi/<name>/json and read the file list for the current stable release, parsing every wheel filename into its Python tag, ABI tag and platform tag. A wheel whose platform tag is any is pure Python and runs on any interpreter, free-threaded or not. A wheel with a real platform tag contains compiled code, and the ABI tag is what decides whether it is installable: cp314 is the GIL build of 3.14, cp314t is the free-threaded build, and they are not interchangeable.

We also read the classifiers list on each project, looking for the four values the packaging ecosystem reserves for this: Programming Language :: Python :: Free Threading :: 1 - Unstable through 4 - Resilient, all four of which are in the current trove-classifiers release.

The install question: 73 of 108

Start with the good news, because there is more of it than the discourse suggests. Of 500 packages, 391 ship only pure-Python wheels. Those install on a free-threaded interpreter without anyone doing anything, because there is no compiled code to rebuild. The free-threading problem is a 108-package problem, not a 500-package one.

Of those 108, 73 ship a cp314t wheel. numpy, pandas, pydantic-core, aiohttp, cryptography, torch, pillow, tokenizers: the load-bearing parts of a modern Python stack are, as of today, installable on free-threaded 3.14 without a compiler.

For scale, compare that with 3.13, where free-threading was still formally experimental: only seven of the same 108 ship a cp313t wheel. Whatever you concluded about ecosystem readiness in 2025, it is out of date.

The 20 that stop you, and what “stop” means

Twenty packages ship a cp314 wheel and no cp314t wheel. Ranked by September downloads:

Package Version Downloads (Sept 2026) Source fallback?
grpcio 1.84.0 319,720,146 sdist
psycopg2-binary 2.9.13 208,775,354 sdist
orjson 3.13.0 180,495,295 sdist (Rust)
zstandard 0.25.0 157,498,843 sdist
google-crc32c 1.9.0 140,796,400 sdist
grpcio-tools 1.84.0 138,194,624 sdist
psycopg-binary 3.3.6 126,278,635 none
black 26.10.1 114,824,778 sdist
snowflake-connector-python 5.0.0 103,373,898 sdist
brotli 1.2.0 84,886,808 sdist

The remaining ten are debugpy, cython, lz4, fastuuid, ruamel-yaml-clib, psycopg2, duckdb, tree-sitter, thrift and ddtrace.

Nineteen of the twenty publish a source distribution, so pip install does not fail. It does something worse: it succeeds, slowly, after invoking a toolchain you did not know you needed. We pulled orjson’s source distribution and asked pip to build it:

pip._vendor.pyproject_hooks._impl.BackendUnavailable:
Cannot import 'maturin'

maturin is the Rust build backend. On a machine with a Rust toolchain, that build runs and eventually produces a working wheel. On a slim CI container, it is a failed deploy with a stack trace that mentions neither Python versions nor free-threading. duckdb wants a C++ compiler and the better part of an hour.

The twentieth is the interesting one. psycopg-binary ships no source distribution at all, and has not in any of its last five releases. That is deliberate: the whole point of the binary variant is that it is pre-built. On a free-threaded interpreter it is therefore not slow, it is impossible. You can reproduce every row of that table in one command, without installing a free-threaded Python:

pip download --no-deps --only-binary=:all: \
  --python-version 3.14 --implementation cp --abi cp314t \
  --platform manylinux_2_28_x86_64 psycopg-binary

That returns ERROR: No matching distribution found for psycopg-binary. Swap in numpy and it downloads numpy-2.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl. Thirty seconds, no new interpreter, and it works against your real requirements file.

The other question: can you actually use threads?

Here is where most readiness checklists stop, and where they go wrong. A cp314t wheel tells you a package installs. It tells you nothing about whether the library is safe to call from several threads at once, which is the only reason to want the free-threaded build.

The ecosystem does have a signal for that, and it is a good one. The official porting guide asks maintainers to declare a level, and defines them precisely: 1 – Unstable means experimentation and feedback only; 2 – Beta means supported but with incomplete documentation of constraints; 3 – Stable means production use with multithreaded testing and documented thread-safety; 4 – Resilient means fully thread-safe.

Across 500 packages, 26 publish one.

Cross-reference the two signals and they barely overlap. Of the 73 packages that ship a free-threaded wheel, 14 declare a level. Fifty-nine of them hand you an installable artefact with no statement about concurrency at all. Meanwhile 11 of the 26 declarations come from pure-Python packages, where there is no wheel tag to inspect and the classifier is the only possible signal: boto3, requests, urllib3, botocore, anyio, s3transfer, prompt-toolkit, mpmath, asgiref, narwhals and packaging. Which leaves 380 of the 391 pure-Python packages in the top 500 saying nothing at all.

Exactly one package declares a level without shipping a free-threaded wheel: zstandard, at 1 – Unstable. That is not a contradiction, it is honesty. The maintainers are telling you the library is not ready even though the build could be made to work. At the other end, charset-normalizer and packaging claim 4 – Resilient, and six sit at 3 – Stable, including cryptography, bcrypt, PyNaCl and coverage.

Why the two signals do not line up

It is tempting to read this as sloppiness. It is not. The wheel tag and the classifier were designed to answer different questions, and the ecosystem has simply never required anyone to answer both.

Wheel ABI tag Trove classifier
Answers Will pip install this? Is it safe to call from threads?
Applies to Compiled packages only (108 of 500) Every package (500 of 500)
Published by 73 of 108 26 of 500
Produced by CI build matrix A deliberate human claim
Fails how? Loudly, at install time Silently, under load, in production

That last row is the one to take away. The install failure is the cheap one: a developer’s laptop, seconds, an error message. The thread-safety failure arrives later, under concurrency, intermittently, in the part of the system where it costs most to diagnose. It is also the one fewer maintainers bother to publish.

This is the same structural problem we found when we traced 35,319 npm releases and found 78 silent licence changes: the registry has a field for the thing you need to know, the field is optional, and optional fields are empty fields.

The platform trap nobody checks

There is a third failure mode, and it is the one most likely to catch a real team, because it passes CI.

Shipping a cp314t wheel is not binary. A package can build free-threaded wheels for some of the platforms it already supports and not others, and 31 of the 73 do exactly that. Twenty-two differ only on iOS, Android or Emscripten, which most server teams can ignore. Nine drop a desktop or server target:

Package Downloads (Sept 2026) Platforms with a GIL wheel but no free-threaded one
rpds-py 463,583,334 Windows arm64
greenlet 369,974,693 Windows arm64
jiter 312,958,613 macOS x86_64
chardet 165,621,246 Windows amd64
onnxruntime 70,379,534 macOS arm64, Windows amd64, Windows arm64
pendulum 53,541,176 macOS arm64, Windows amd64
numba 52,272,647 Windows arm64
llvmlite 52,183,739 Windows arm64
ormsgpack 42,145,752 musllinux (Alpine) x86_64 and aarch64

Read that against the 3.15 release note we opened with. onnxruntime ships free-threaded wheels for Linux and nothing else, and 3.15 has just made free-threading the default install on macOS. The collision is not hypothetical; it is scheduled. ormsgpack is the container version of the same story: your Debian-based image is fine, your Alpine image is not.

Python 3.15, abi3t, and why only three packages have it

3.15 also brings PEP 803, a stable ABI for free-threaded builds, tagged abi3t. In principle this is the fix for the whole wheel-matrix problem: build once, install on every future free-threaded Python, the way abi3 already works for GIL builds.

Eleven days after release, three packages in the top 500 already ship abi3t wheels: cryptography, hypothesis and ast-serialize. cryptography’s filenames carry both tags at once, which is what the transition is supposed to look like.

Why only three? Because the tooling is not there yet, and it says so. Cython 3.3.0, released 22 August 2026, records in its own changelog:

Changes were made to adapt to Python 3.15 and its Limited API. This does not yet cover abi3t.

Cython is how a large share of the compiled packages on this list are built. Until it covers abi3t, most maintainers cannot adopt the stable ABI even if they want to. Cython, incidentally, is itself one of the 20 with no free-threaded wheel.

Counting cp315t and abi3t together, 59 of the 108 compiled packages already install on free-threaded 3.15. Fifteen that have a 3.14 free-threaded wheel do not yet have a 3.15 one, including torch, onnxruntime, numba, triton and tokenizers. Eleven days in, that is a lag, not a verdict. Re-run the check in January.

What to do with this

You do not need to adopt free-threading to need an answer here. 3.15 has moved the decision from your platform team to whoever next installs Python on a Mac.

  1. Run the resolution check against your real requirements file. The pip download command above works today, on your current interpreter, and tells you in under a minute which dependencies have no free-threaded wheel.
  2. Separate “installs” from “safe”. For every compiled dependency that passes, check the classifier on its PyPI page. If there isn’t one, and you intend to call it from multiple threads, you are the one testing it.
  3. Check the platforms, not just the package. A free-threaded wheel on Linux x86_64 is not one on macOS arm64. Run the check once per platform tag you ship to, including musllinux if you build Alpine images.
  4. Pin the interpreter, explicitly, in CI. The expensive failure is a base image quietly changing which build it ships. The same class of problem has already played out in Node, where a runtime and its own bundled package manager disagreed about what was installable.
  5. If you maintain a package, publish a classifier. It costs one line of metadata. 1 – Unstable is a perfectly respectable answer, and it is infinitely more useful to your users than silence.

What this measurement does not tell you

Three honest limits. We read the current stable release of each project on one day, so a maintainer who ships a free-threaded wheel on 21 October is counted as not having one. The absence of a classifier is not evidence of thread-unsafety, only of an unpublished claim; plenty of those 59 packages are fine, and we have no way to tell you which. And “installs” is the floor, not the ceiling: a cp314t wheel that was never tested under concurrency will install perfectly and still be the thing that wakes you at 3am.

The repeatable part is the install check, and that is the part worth automating. Everything above came from two public endpoints and one pip flag.

REPTILEHAUS builds and maintains Python services, data pipelines and AI infrastructure for teams who would rather find this out in CI than in production. If you want this audit run against your own stack, or your build matrix reworked before 3.15 reaches your base images, get in touch.


Measured 20 October 2026 against the top 500 PyPI projects by September 2026 downloads. Package metadata from the PyPI JSON API; package list from the top-pypi-packages snapshot of 1 October 2026.

📷 Photo by Bozhin Karaivanov on Unsplash