Restricting "open ended" releases on PyPI?

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).

2 Likes

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
7 Likes

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 :slightly_smiling_face:

4 Likes

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.*.

1 Like

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.

1 Like

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.

1 Like

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.


  1. 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. ↩︎

2 Likes

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.

7 Likes

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.

3 Likes

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.


  1. be it just an approve button or creating a special token ↩︎

3 Likes

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!

3 Likes

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).

4 Likes

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.


  1. If it’s self-serve, doesn’t this carry the same security concerns? If it’s not, doesn’t that increase index support burden? ↩︎

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.

6 Likes

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.

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 :man_shrugging:

2 Likes