Will the value be global, or can it be different per-project or per-org?
So when Python 3.15 comes out, every project that wants to ship 3.15 wheels will have to publish a new release, and rebuild wheels for all supported versions, even if there have been no project changes in the meantime? That seems like it could delay availability of 3.15 wheels to me… (especially for more niche projects).
Yes, and the common practice today is to make a full new release.
Looking at when cp314 wheels were added to the top 15k packages:
| Metric | Value | % of packages | % of cp314 releases |
|---|---|---|---|
| Packages scanned | 15,000 | 100% | - |
Ship cp314 wheels (rest are pure-Python / abi3) |
1,094 | 7.29% | - |
Releases with cp314 wheels |
7,958 | 53.05% | 100% |
| Distinct packages doing late additions | 65 | 0.43% | 0.82% |
Releases where cp314 added late (>1 day post-publication) |
69 | 0.46% | 0.87% |
| Late additions > 3 days | 65 | 0.43% | 0.82% |
| Late additions > 7 days | 60 | 0.40% | 0.75% |
| Late additions > 14 days | 56 | 0.37% | 0.70% |
| Late additions > 1 year | 10 | 0.07% | 0.13% |
| Median late gap / Max late gap | 106d / 1,332d | - | - |
| Rank | Packages |
|---|---|
| Top 100 | 0 |
| 101–1,000 | 3 |
| 1,001–5,000 | 14 |
| 5,001–15,000 | 48 |
All late cp314 additions
| Rank | Package | Version | Released | cp314 added | Gap |
|---|---|---|---|---|---|
| 14034 | kivy-deps-glew | 0.3.1 | 2022-02-15 | 2025-10-09 | +1332d |
| 12043 | uefi-firmware | 1.11 | 2023-08-10 | 2026-06-04 | +1028d |
| 8303 | dynet38 | 2.2 | 2024-01-23 | 2025-12-29 | +706d |
| 8200 | nagisa | 0.2.11 | 2024-01-28 | 2025-12-29 | +701d |
| 4426 | jenkspy | 0.4.1 | 2024-06-03 | 2026-02-13 | +620d |
| 11381 | atomic-dict | 0.5.0 | 2024-08-17 | 2026-01-08 | +510d |
| 4523 | httpstan | 4.13.0 | 2024-07-03 | 2025-11-14 | +499d |
| 14242 | cypari | 2.5.5 | 2024-09-15 | 2025-12-10 | +451d |
| 9239 | panda3d | 1.10.15 | 2024-11-08 | 2025-12-01 | +388d |
| 11027 | clingo | 5.8.0 | 2025-04-03 | 2026-04-14 | +376d |
| 12786 | poselib | 2.0.4 | 2024-08-07 | 2025-07-27 | +353d |
| 13604 | fxrays | 1.3.6 | 2024-11-17 | 2025-10-29 | +346d |
| 14871 | low-index | 1.2.1 | 2025-01-13 | 2025-12-10 | +331d |
| 14074 | knot-floer-homology | 1.2.2 | 2025-01-13 | 2025-12-10 | +331d |
| 5005 | stpyv8 | 13.1.201.22 | 2025-01-09 | 2025-11-03 | +298d |
| 8148 | scrypt | 0.8.27 | 2024-10-11 | 2025-08-02 | +295d |
| 9055 | py-ed25519-zebra-bindings | 1.2.0 | 2024-12-09 | 2025-09-02 | +267d |
| 8981 | py-bip39-bindings | 0.2.0 | 2024-12-09 | 2025-09-02 | +267d |
| 9410 | pyreadr | 0.5.3 | 2025-03-04 | 2025-10-27 | +237d |
| 12086 | cobs | 1.2.2 | 2025-07-20 | 2026-03-09 | +232d |
| 13428 | silx | 2.2.2 | 2025-04-08 | 2025-10-31 | +206d |
| 14695 | kdepy | 1.1.12 | 2025-04-17 | 2025-10-29 | +194d |
| 11333 | python-libsbml | 5.20.5 | 2025-05-05 | 2025-11-11 | +190d |
| 12905 | fpylll | 0.6.4 | 2025-05-29 | 2025-11-15 | +170d |
| 6440 | symengine | 0.14.1 | 2025-04-21 | 2025-09-14 | +146d |
| 5843 | qiskit-aer | 0.17.2 | 2025-09-17 | 2026-02-04 | +140d |
| 2222 | pyqt6-sip | 13.10.2 | 2025-05-23 | 2025-10-08 | +138d |
| 9009 | mdtraj | 1.11.0 | 2025-06-25 | 2025-10-22 | +120d |
| 662 | statsmodels | 0.14.5 | 2025-07-07 | 2025-10-30 | +115d |
| 8135 | winrt-runtime | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 8812 | winrt-windows-devices-enumeration | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 9536 | winrt-windows-devices-bluetooth-genericattributeprofile | 3.2.1 | 2025-06-06 | 2025-09-20 | |
| +106d | |||||
| 9544 | winrt-windows-devices-bluetooth-advertisement | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 9482 | winrt-windows-devices-bluetooth | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 8492 | winrt-windows-foundation-collections | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 8284 | winrt-windows-foundation | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 10496 | winrt-windows-devices-radios | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 8867 | winrt-windows-storage-streams | 3.2.1 | 2025-06-06 | 2025-09-20 | +106d |
| 2138 | brotlicffi | 1.2.0.0 | 2025-11-21 | 2026-03-05 | +104d |
| 2164 | pycairo | 1.28.0 | 2025-04-14 | 2025-07-24 | +100d |
| 3922 | phik | 0.12.5 | 2025-07-17 | 2025-10-10 | +85d |
| 2686 | pyreadstat | 1.3.1 | 2025-08-14 | 2025-10-29 | +76d |
| 1301 | geventhttpclient | 2.3.4 | 2025-06-11 | 2025-08-24 | +74d |
| 6439 | cmeel-boost | 1.89.0 | 2025-08-22 | 2025-10-16 | +54d |
| 5842 | eigenpy | 3.12.0 | 2025-08-23 | 2025-10-15 | +53d |
| 9168 | gdstk | 0.9.61 | 2025-08-28 | 2025-10-20 | +53d |
| 5887 | pygame-ce | 2.5.5 | 2025-06-07 | 2025-07-28 | +51d |
| 4593 | saxonche | 12.8.0 | 2025-07-03 | 2025-08-12 | +40d |
| 13400 | flydsl | 0.1.4 | 2026-04-20 | 2026-05-30 | +40d |
| 1889 | ckzg | 2.1.5 | 2025-09-30 | 2025-11-06 | +37d |
| 11057 | lets-plot | 4.8.2 | 2025-12-19 | 2026-01-21 | +33d |
| 12760 | fckitlib | 0.14.1.dev2 | 2025-09-29 | 2025-10-31 | +32d |
| 12760 | fckitlib | 0.14.1.dev1 | 2025-09-29 | 2025-10-31 | +32d |
| 527 | mysql-connector-python | 9.6.0 | 2026-01-21 | 2026-02-10 | +20d |
| 9777 | openmm | 8.4.0b0 | 2025-09-20 | 2025-10-09 | +19d |
| 2467 | nvidia-cudnn-frontend | 1.18.0 | 2026-01-27 | 2026-02-11 | +15d |
| 7191 | daqp | 0.7.2 | 2025-07-29 | 2025-08-11 | +13d |
| 1368 | blake3 | 1.0.7 | 2025-09-29 | 2025-10-11 | +12d |
| 805 | av | 16.0.0 | 2025-10-05 | 2025-10-13 | +8d |
| 527 | mysql-connector-python | 9.7.0 | 2026-04-23 | 2026-04-30 | +7d |
| 9436 | rnet | 3.0.0rc8 | 2025-10-03 | 2025-10-09 | +6d |
| 14065 | ayafileio | 0.1.7.post1 | 2026-03-30 | 2026-04-04 | +5d |
| 7949 | stim | 1.16.dev1776477515 | 2026-04-18 | 2026-04-21 | +4d |
| 13755 | rqdatac | 3.4.2 | 2026-01-09 | 2026-01-13 | +4d |
| 4593 | saxonche | 12.9.0 | 2025-09-12 | 2025-09-15 | +3d |
| 805 | av | 16.1.0 | 2026-01-09 | 2026-01-11 | +2d |
| 9719 | gdsfactoryplus | 1.3.12 | 2026-01-26 | 2026-01-28 | +1d |
| 4787 | nvtx | 0.2.14 | 2025-11-27 | 2025-11-28 | +1d |
| 1777 | catboost | 1.2.9 | 2026-02-17 | 2026-02-18 | +1d |
Possibly you could reset the timer whenever a wheel is added to a release? So the rule becomes “N days no additions —> lock release”
Projects that are making use of this would most likely want to add new wheels for each new release of CPython. You can see that from the upload dates (just below each wheel filename) for the project at the top of Hugo’s list. The time limit would have to be at least 1 year for a project to be able to add new wheels at the time of each CPython release.
A limit of 1 year is a bit long though for other purposes. If the limit is more like 1 week then:
- That is still plenty of time to upload all files and complete a normal release.
- It is also plenty of time for any automated security scanning
- That is also a reasonable time limit for an installer cooldown (e.g. you could tell pip, dependabot etc not to use versions younger than 2 weeks by default).
The downside of a 2 week limit is that projects in Hugo’s list would need to make full new releases with new versions rather than just uploading new wheels to an existing version:
- Making a new release is a more involved process for the project (possibly causing delay) than just rebuilding with a newer Python version.
- Publishing new releases has a ripple overhead of downstream version bumping (dependabot PRs and so on).
- If the only purpose is for a new CPython version then the new release would have effectively duplicate wheels for the previous CPython versions increasing total filesize on PyPI.
On the latter point projects are limited to 10GB in total filesize on PyPI. It is easy to get there with ABI-specific wheels. If a wheel is 20MB and you want to support 5 CPython versions and 5+ OS/arch (Windows x64/arm64, MacOS intel/arm, manylinux x64/aarch64, musllinux, …) then you easily have 500MB of wheels in one release (see e.g. numpy) making a cap of 20 releases total for the project.
Thanks - actual data beats my gut instinct every time ![]()
Why would you need to rebuild them? Just bump up the version metadata of the existing wheels and repost.
You could also create version policy where anything posted to, say the 4th digit is new platform / python releasees and then tell people who want to pin but get new builds to pin against X.Y.Z.*.
With a resetting timer, you could set it to something much shorter than two weeks, just long enough to account for slow rolling CI. And then there would be a natural implementation of:
It would just be a timer reset.
packages that don’t want to do this should probably stick to the stable abi (abi3). If they can’t do that, then they probably should have a new release anyway.
Some packages, such as regex, use functions that not available in the stable ABI. For example, regex uses PyUnicode_DATA so that it can get to the string’s data directly, without which it would be noticeably slower.
That’s fine, but I’d expect a library not using the stable ABI to have at least one release a year even if there are no changes in their code to build against the latest python[1]. For projects that are “finished” (not unmaintained, just no reason to change their code), this can be automated.
The modern view is that the build system parameters supported are part of a project. For a project shipping wheels, that would mean they require at minimum changing the versions of python to build wheels for. ↩︎
I can see I’m outnumbered here but I really appreciated that when Windows+macOS arm and each many/musllinux variant came out, I was able to do a quick pytest then pip wheel .; twine upload for each project and PyPI didn’t make a fuss by insisting that I create silly no-source-change releases.
Urm, that would put us right back where we started… Our would-be malicious publisher would simply create a X.Y.Z.1 release for all the soft pins to pick up.
Any idea what issues you would accept given that we’re already writing off the obvious ones?
You’d think. I’ve got quite a few compiled projects in this state and I’ve found that the automation itself costs way more maintenance than it’s worth. 1 year is a long time for a GitHub Actions workflow to rot.
That’s a reasonable scenario for explicitly resetting the timer, though, since presumably you’d have to log in to PyPI to get a temporary publishing token (because if you’re keeping a global publishing token around any place it may get stolen, you’re simultaneously the reason we need this and the reason we can’t have it).
I’m assuming that if you’re properly using OIDC it’s just as easy to cut an entire new release as it would be to add one single file, because in all the simple cases, it is. You have to go out of your way to make it easier to do it this way, and it’s more likely that the team of organised criminals who have gotten hold of your GitHub token are going further out of their way to do it than you.
I’m not quite sure I follow what you’re saying but if the general consensus is that leaked tokens/compromised workflows rather than full PyPI account takeovers are the target and therefore that having a bypass for this restriction on the PyPI web UI [1] wouldn’t defeat the purpose of said restriction then I have absolutely no issue with this proposal. I already use single-use tokens and log into PyPI for every release to generate them so this wouldn’t affect me at all.
be it just an approve button or creating a special token ↩︎
FWIW I don’t think there’s anything preventing implementing the analytics portion of this right now, regardless of what the outcome of this discussion is.
My only concern with the timer reset thought is that it’s somewhat harder to understand, and I’m not sure how much value it actually provides. It does mean that we could have a much shorter time scale (say 3 days) so the window is smaller, but 3 days versus 14 days doesn’t feel super significant to me.
That is basically the trade off here. It’s not really that fundamentally different than when we decided that you couldn’t delete and then re-upload the same file over and over again-- it’s finding the balance between giving authors the freedom to do as much as possible for what makes sense for their project and providing a platform for end users that is as consistent as possible between different projects so they know what to expect without having to deep dive on the release processes of every project.
I did not interpret the proposal by @sethmlarson to include a mechanism for authors to explicitly bypass the lock-out, though it’s an interesting idea, and I’d have to think about it a bit to figure out how I feel about it. It does entirely depend on exactly what we’re trying to protect against with this proposal though!
It might be reasonable to allow for specifically adding wheels that are built for the latest python version within some predetermined time window after that version of python has it’s .0 release, rather than having a more general bypass if the bypass is meant to assist with that specific concern on impact for package maintainers.
Those wheels won’t get picked up for preexisting installs (well, I guess they might for preexisting alpha, beta, and rc installs), so some of the concerns about who gets caught off guard by new wheels for an already existing release don’t apply in that narrow case.
That feels too special-case for me - e.g. why do it for versions and not new platforms? But more importantly, why would we want some global timer to determine whether an upload is going to succeed or fail? Imagine missing it by thirty seconds.
If there’s no self-serve mechanism, then there’ll be support requests. That’s basically the choice here - either the admins handle requests (which is not unreasonable to start with), or there’s a way for package owners to do it themselves (and provided it’s on the other side of a MFA check, it doesn’t significantly weaken the whole thing IMHO).
The alternative I was picturing is not having a bypass mechanism at all[1], and this allowing the use case someone who has had an issue with CI bitrot wanting a bypass for something relatively narrow. It is extremely special cased, but it’s narrow enough to avoid the major concerns that have led to wanting to restrict open ended releases, so it feels like a pragmaticly possible concession. I’m not attached to the idea if it feels too special for people to be okay with or people have other issues with it.
If it’s self-serve, doesn’t this carry the same security concerns? If it’s not, doesn’t that increase index support burden? ↩︎
If there’s no self-serve mechanism, then there’ll be support requests. That’s basically the choice here - either the admins handle requests (which is not unreasonable to start with), or there’s a way for package owners to do it themselves (and provided it’s on the other side of a MFA check, it doesn’t significantly weaken the whole thing IMHO).
I don’t think the alternative is having admins handle requests FWIW, like @mikeshardmind said, I think the alternative is “you just can’t do that”, the same way that you can’t delete and re-upload the same file today (even though you could in the past).
Whether having a bypass makes sense depends on exactly what we’re trying to protect against. A self serve bypass through the PyPI Web UI is a higher barrier to entry for a would be attacker than an API token.
If the primary concern is things like compromised CI tokens and the like, then allowing a bypass via the Web UI means that you require the username/password + MFA credential (which are all not likely to be embedded in someone’s CI pipeline anywhere, given those credentials don’t allow you to do an actual upload).
That does still mean that a compromised user account can still (obviously) publish outside of the restriction, so it comes down to whether that’s an avenue of attack that we’re particularity worried about here and how this particular risk compares to the other powers that compromised account has.
I suspect that the self serve option doesn’t drastically reduce the overall benefit of the restriction here-- I would guess that, by far, the credentials most likely to be stolen are the API keys. However, it also appears to me that the use case we’re trying to enable by adding such a feature isn’t particularly widespread. The numbers from @hugovk suggest that only 0.4% of the top 15,000 packages would have been affected in recent history.
So I’d almost look at it another way, every feature we add has a cost both initially and over the long term, and it’s hard for me to come up with a super compelling justification for paying that long term cost for such a small number of projects other than possibly to make people feel better about the restriction overall.
I would guess that, by far, the credentials most likely to be stolen are the API keys.
API keys and access to OIDC via stolen CI pipeline credentials have both been stolen and misused in the recent weeks, so yes, these are the primary concerns. I’m not aware of any compromises of user account login or MFA, so unless someone with more inside knowledge of attacks against PyPI wants to state that it has happened, I think we can safely assume that these are the escape hatch.
I think the alternative is “you just can’t do that”, the same way that you can’t delete and re-upload the same file today (even though you could in the past).
Except that one’s easier to justify because people understand content-based addressing (you also can’t upload two byte-for-byte identical files, which admittedly can only happen with invalid metadata, but it still can’t be done).
Some amount of people will encounter this arbitrary (as in, not directly driven by technology choice) limitation, some amount of those will complain/question it here, others will complain on socials, and some will complain directly to any email address that can be found. If this is genuinely going to be a small number, then not having any workaround is fine ![]()