What to do when you botch a release on PyPI
So you made a release on PyPI and there's a mistake (we've all been there). It can be as big as the whole release is bad, to just a spelling mistake in the README. Luckily there are things you can do to deal with various scenarios.
The whole release is bad
Let's say there's a bug so bad that no one should use a release. In that case, you should yank the release. What this does is it tells PyPI to not list the version on the site. It also tells pip to not consider it when trying to resolve whether the version is applicable to install. What it does not do is make the files go away; they are merely hidden. So if someone explicitly asks for those files then they can still get them (this is so if for some odd reason people really want those files they will not be suddenly broken by you yanking the release).
You yank releases in the admin page for your project. Next to each release there is a blue "Options" button. Click that for a drop-down menu and there will be a "Yank" option to select to yank the release.
There's a non-code problem
Let's say you make a release where you misspelled something in the README, or some other equivalent mistake which doesn't affect the semantics of the code. In that case, you can do a post-release.
The version specifier spec says you can actually end a version number in a
.post component to tell installers like pip that you don't need to upgrade a package with the same non-post version, but that if this is a new install you want the post-release version. So if you just released version
1.2.3, realized you have a non-semantic thing to fix, and released
1.2.3.post1, installers will only install
1.2.3.post1 if you don't already have
1.2.3 installed. Since semantically the two version are supposed to be the same, there's no point in reinstalling a package just to fix e.g. a typo in a README file.
A wheel file wasn't compiled properly
Let's say you made a release where one of the wheels was improperly compiled. You don't have to yank the entire release to fix this problem, and instead you can "replace" the bad wheel file.
Now I used the scare quotes on "replace" because you can't actually delete a single wheel file, but what you can do is tell installers to use a newer wheel file instead via a build number, effectively shadowing the old wheel file. When wheel tags match in all other ways than build number, installers always use the wheel file with the highest build number. And because you can always upload more files to a release, you can then simply upload your fixed wheel file with a different, higher build number to shadow the broken wheel file.
Repeatability != reproducibility
This may shock you and have you suddenly worrying about reproducibility. But a key thing to note is when you tell an installer to get e.g.
gidgethub==5.0.1, what you're doing is telling the installer to get the best file that meets that requirement, not necessarily the same file that you installed last time. Even without a wheel file being shadowed by one with a higher build number, a release may simply get better wheels for your platform since you last installed. So while you may be able to repeat the installation, it doesn't mean you reproduced it with the same bits as before. The only way to get full reproducibility to specify the exact file to install (if that matters to you; and if it does then I have a proposed PEP to help with that).
Support pre-release/new versions of Python
One very nice benefit of this shadowing of wheel files is it lets you push wheels that support a pre-release version of Python without the fear of having to do a new release to repleace the same wheels later in case a breaking compiler change is made in Python itself. Since you can always push a version of the wheel with a higher build number, you can push up a new wheel to replace the older one that no longer works (e.g. shadow the wheel that worked on Python 3.10.b4 with one that works with Python 3.10.rc1 if it was just a compiler issue). If there are semantic changes required, though, then you will want to do a new release instead of a single file change. This also extends to the final/stable versions of Python where you want to add a wheel just for that new version without cutting a brand-new release (once again, when it's just a matter of compiling a new wheel and not semantic changes).