Python 3.15 Is Out. I Checked the Top 500 PyPI Packages: 97% Install From a Wheel, 12 Still Run Build Scripts
Key takeaways
- 485 of the top 500 PyPI packages (97.0%) install from a wheel on Python 3.15, and 12 of the other 15 download a source archive and run its build code.
- PyYAML is the biggest of those 12, with 910,851,659 downloads in 30 days.
- Only 102 of the 500 ship compiled code, and of those 84 (82.4%) have a compiled wheel that works on 3.15 while 18 (17.6%) do not.
- Only 60 of the 102 compiled packages (58.8%) have a free-threaded wheel for python3.15t.
- With --only-binary :all:, 14 of 19 problem packages refused to install, 4 fell back to pure Python, and snowflake-connector-python silently resolved to 1.9.1 instead of 5.1.0.
Python 3.15.0 shipped on October 9, and 3.15.1 followed on October 21. Before any upgrade I ask one question: what will pip install actually do on the new interpreter?
I pulled the 500 most-downloaded packages on PyPI and classified every file in each one’s latest release. 485 of 500 (97.0%) install from a wheel on 3.15. The other 15 matter more than the percentage suggests. 12 of them will download a source archive and run its build code on your machine, and the biggest one is PyYAML, with 910,851,659 downloads in 30 days.
TL;DR (for non-technical readers)
- Python 3.15 is the new version of Python. Most popular add-on packages (97% of the top 500) are ready and install as finished, pre-built files.
- 12 popular packages are not ready yet. Installing them on 3.15 means downloading their source code and running their build scripts on your computer, which needs a compiler and trusts that code completely.
- On Windows the picture is almost identical (96.8% ready), but a typical Windows laptop has no C compiler, so most of those 12 installs would fail there after starting to run code.
- There is a one-line setting that refuses to run build scripts. It is worth turning on, but it can also quietly install a much older version, so check what you get.
What changed in 3.15 (verified)
I only list what I confirmed on python.org and docs.python.org today:
- Explicit lazy imports (PEP 810). New syntax,
lazy import jsonandlazy from pathlib import Path, defers loading a module until the name is first used. There is also a global switch,-X lazy_importsorPYTHON_LAZY_IMPORTS. - UTF-8 by default (PEP 686).
open()without anencodingargument now uses UTF-8 regardless of the system locale. You can opt out withPYTHONUTF8=0or-X utf8=0. - Free-threading. The no-GIL build is still a separate interpreter (
python3.15t). New in 3.15 is a stable ABI for it, PEP 803 (abi3t), so one extension build can serve many free-threaded versions. The docs put single-thread overhead at about 1% on macOS aarch64 up to 8% on x86-64 Linux. - Also new: a built-in
frozendict(PEP 814) and a sampling profiler in the newprofilingpackage (PEP 799).
On the tooling side, uv 0.13.0 (October 9) added CPython 3.15.0 and made 3.15 its default. I used uv 0.13.5 with CPython 3.15.1.
Method
On October 25, 2026, on a Windows 11 laptop:
- Took the top 500 rows of Hugo van Kemenade’s 30-day top-pypi-packages list (snapshot dated October 1).
- Fetched
https://pypi.org/pypi/<name>/jsonfor each, 20 at a time, cached to disk, and parsed every wheel tag of the latest release withpackaging. Yanked files were excluded. - Put each package in the first matching bucket:
cp315wheel,abi3wheel built for 3.X ≤ 3.15, pure Pythonpy3-none-any, a Python-agnostic binary wheel such aspy3-none-win_amd64, sdist only, or nothing installable. - For 30 packages (all 19 problem packages plus 11 random controls), ran
uv pip install --dry-run --only-binary :all:against a real 3.15.1 venv, and the same resolution for Linux withuv pip compile --python-platform x86_64-manylinux_2_28.
I did not let any sdist build run. That is deliberate: the question is whether you must execute someone else’s build code, so I read each sdist’s pyproject.toml and file list statically.
Results

| How it installs on 3.15 | Packages | Share | Share of downloads |
|---|---|---|---|
Pure Python wheel (py3-none-any) | 395 | 79.0% | 77.8% |
cp315 wheel | 65 | 13.0% | 15.9% |
abi3 wheel | 19 | 3.8% | 3.8% |
py3-none-<platform> binary | 6 | 1.2% | 0.7% |
| Has wheels, none for 3.15: builds from sdist | 11 | 2.2% | 1.6% |
| No wheels at all: always builds | 1 | 0.2% | 0.0% |
| No 3.15 wheel and no sdist | 3 | 0.6% | 0.2% |
Pure Python flatters the headline. Only 102 of the 500 ship compiled code, and that is where 3.15 support actually costs maintainers work. Of those 102, 84 (82.4%) have a compiled wheel that works on 3.15. 18 (17.6%) do not.
Free-threading is further behind. 57 packages ship a cp315t wheel and only 3 ship the new abi3t (cryptography, ast-serialize, hypothesis). Counting pure Python, 464 of 500 (92.8%) install from a wheel on python3.15t, but only 60 of the 102 compiled packages (58.8%) have a free-threaded wheel.
The biggest blockers by download count

Ten of the 19, covering each failure type (the chart has all):
| Rank | Package | 30-day downloads | Newest wheel | On 3.15 |
|---|---|---|---|---|
| 13 | pyyaml | 910,851,659 | cp314 | sdist build |
| 61 | frozenlist | 417,051,290 | cp314 | pure-Python fallback |
| 177 | zstandard | 157,498,843 | cp314 | sdist build |
| 248 | black | 114,824,778 | cp314 | pure-Python fallback |
| 261 | snowflake-connector-python | 103,373,898 | cp314 | sdist build |
| 294 | watchdog | 87,261,951 | cp313 | py3-none fallback |
| 318 | lz4 | 75,441,790 | cp314 | sdist build |
| 334 | onnxruntime | 70,379,534 | cp314 | no install |
| 366 | torch | 61,685,076 | cp314 | no install |
| 428 | pyspark | 48,293,347 | none | sdist only |
The “fallback” rows are a quiet trap: frozenlist, black, watchdog and pytokens install on 3.15, but you get the slower pure-Python code instead of the compiled build you had on 3.14, with no warning.
torch, onnxruntime and triton have no 3.15 wheels and no sdist, so they simply fail to resolve. Loud is the good kind of failure.
The supply-chain angle: build scripts at install time
A wheel is unpacked. An sdist is executed. When pip or uv has to build an sdist, it runs the package’s build backend, and in 10 of the 11 sdists I inspected that includes a setup.py. Nine use setuptools, PyYAML uses a custom in-tree backend, and fastuuid uses maturin, which needs a Rust toolchain. 10 of the 11 contain C, C++, Cython or Rust source. Every one of those builds is arbitrary code with your user’s permissions, pulled from whichever release resolved, including its build dependencies.
pyspark is the odd one: it ships no wheels at all, so its setup.py runs on every Python version, not just 3.15.
You can refuse this. Both flags below are verified against current docs and uv pip install --help:
# pip: wheels only, for every package
pip install --only-binary :all: -r requirements.txt
# uv pip: same flag, or the uv-native switch
uv pip install --only-binary :all: -r requirements.txt
uv pip install --no-build -r requirements.txt
For uv projects, put it in pyproject.toml, optionally with a per-package list:
[tool.uv]
no-build = true
# or, narrower:
# no-build-package = ["pyyaml", "zstandard"]
Here is what that did in my sample:

- All 11 controls resolved.
- 14 of 19 problem packages refused to install on Windows rather than build. That refusal is the point.
- 4 quietly fell back to the pure-Python wheel.
- 1 silently downgraded.
snowflake-connector-pythonresolved to 1.9.1 instead of 5.1.0, because that old release, which still supports Python 2.7, ships a pure-Pythonpy2.py3-none-anywheel. On Linux,ddtracewent from 4.16.0 to 4.13.1 the same way.
So “wheels only” is a security control, not a correctness guarantee. Pin versions or check the resolved set after you turn it on.
Windows notes
On win_amd64, 484 of 500 (96.8%) install from a wheel and 82 compiled packages (80.4% of 102) have a 3.15 wheel. The differences are small: pyzmq and ast-serialize ship cp315 elsewhere but abi3 on Windows, caio falls back to pure Python, and uvloop has no Windows wheels for any version.
The real Windows difference is the toolchain. My test laptop has no MSVC (cl.exe is not on the PATH), which is normal for a developer machine that never needed one. With 10 of the 11 sdists containing native source, most of those builds would fail anyway, after they have already started running code.
The other Windows change is PEP 686. On Windows, open("file.txt") used the ANSI code page (often cp1252) before; on 3.15 it is UTF-8. Code that wrote files without encoding= on 3.14 and reads them on 3.15 can see mojibake or a UnicodeDecodeError. Run your tests with -X warn_default_encoding to find every call that relies on the default, then pass encoding= explicitly (or encoding="locale" if you really want the old behavior).
Lazy imports help on Windows too. With -X lazy_imports=all, a script that imports requests and touches requests.Session dropped from 993.8 ms to 466.9 ms (53% less, median of 21 runs). httpx dropped 37% and rich.console 26%. Eager 3.15 was about the same as 3.14 (993.8 ms vs 992.7 ms for requests). This laptop is slow in absolute terms (an empty pass takes about 170 ms), so trust the ratios.
A practical upgrade checklist
- Install the interpreter:
uv python install 3.15. - Resolve your lock file wheels-only:
uv pip compile requirements.in --python-version 3.15 --only-binary :all:. Every failure is a package that would have run build code. - Diff the resolved versions against your 3.14 lock. Look for downgrades like snowflake-connector-python 5.1.0 to 1.9.1.
- Check for silent pure-Python fallbacks (frozenlist, black, pytokens, watchdog) if you care about their speed.
- Set
no-build = true(orno-build-package) in CI so a missing wheel fails the build instead of executing an sdist. - On Windows, run tests with
-X warn_default_encodingand fix every implicit encoding. - If you want
python3.15t, check your compiled dependencies separately; only 58.8% have a free-threaded wheel. - Measure your CLI with
-X lazy_imports=allbefore rewriting imports.
Caveats
- This is the latest release only. Pinned older versions are a different, usually worse, story.
- The ranking snapshot is from October 1; new 3.15 wheels may appear any day. Re-run before you quote these numbers.
- Wheel tags say “compatible”, not “works”. I did not run test suites.
- Some blockers exclude 3.15 on purpose via
requires-python: backports-zstd (<3.14) is not needed on 3.15 at all, and ddtrace and triton declare<3.15. - The 30-package resolution test is a sample; the classification covers all 500.
- I never executed an sdist build, so “would fail without MSVC” is inferred from the source files, not observed. Some packages, PyYAML among them, can fall back to pure Python when the extension does not compile, but their
setup.pystill runs first.
Do you already run CI with --only-binary :all: or no-build = true, and if not, what is stopping you: a specific sdist-only dependency, or just never having thought about install-time code?
Related: uv check, type checking for single-file Python scripts and a 1-day cooldown would have blocked 70% of 2026’s hijacked npm releases.
Frequently asked questions
- Does PyYAML have wheels for Python 3.15?
- Not yet. Its newest wheel is cp314, so on 3.15 it builds from an sdist, which runs its custom in-tree build backend and a setup.py on your machine. PyYAML had 910,851,659 downloads in 30 days.
- How do I stop pip or uv from building sdists?
- With pip use pip install --only-binary :all: -r requirements.txt. With uv pip use --only-binary :all: or --no-build. In a uv project set no-build = true under [tool.uv], or narrow it with no-build-package = ["pyyaml", "zstandard"].
- Is it safe to turn on wheels-only installs?
- It is a security control, not a correctness guarantee. In the sample, snowflake-connector-python resolved to 1.9.1 instead of 5.1.0 because that old release ships a pure-Python py2.py3-none-any wheel, and on Linux ddtrace went from 4.16.0 to 4.13.1. Pin versions or check the resolved set.
- How many of the top 500 PyPI packages install from a wheel on Python 3.15?
- 485 of 500 (97.0%). Of the other 15, 12 build from an sdist and 3 have neither a 3.15 wheel nor an sdist, including torch, onnxruntime and triton, which fail to resolve. On win_amd64 the figure is 484 of 500 (96.8%).
About the author
Security Engineer & Technical Writer
Security engineer and technical writer based in Batam, Indonesia. I review and harden MCP servers and AI agents, build security automation, and write up what holds and what fails. I also train on DNS security as a PANDI .id Academy trainer.
Related articles
-
9 min read
A 1-Day Cooldown Would Have Blocked 70% of 2026's Hijacked npm Releases. Against Typosquats, 7 Days Still Misses 45%
I replayed 11,556 malicious npm and PyPI packages from 2026 against a dependency cooldown. Hijacks die fast, typosquats don't, and fixes pay the bill.
- Security Engineering
- Engineering Practice
-
9 min read
I Read the CI of 300 Popular Repos. 145 Still Run Third-Party Actions by Tag or Branch
I parsed 4,750 workflows from 300 popular repos. 145 of 265 that use third-party actions still reference one by a movable tag or branch.
- Security Engineering
- Engineering Practice
-
6 min read
UUIDv7 Is a Great Database Key and a Bad Clock: 4 Tests Before You Migrate
UUIDv7 is a great database key but a poor clock. Four quick tests, what the embedded timestamp leaks, and a rule of thumb for using UUIDv7 as a primary key.
- Security Engineering
- Engineering Practice
All writing | RSS | Markdown