Open Source Security

PyPI Package Yanking Policies Analyzed

Yanking is PyPI's narrow, deliberately blunt tool for dealing with broken releases. A close analysis of what it does, what it doesn't do, and when to use it instead of a delete.

Shadab Khan
Security Engineer
7 min read

Yanking is one of those PyPI features that most users have never consciously used but that touches the supply chain in subtle ways. It was introduced under PEP 592 ("Adding Yank Support to the Simple API"), accepted in May 2019 and implemented in Warehouse and pip through the rest of that year. Five years on, yanking has become the default response to a broken release on PyPI, and the way it interacts with dependency resolution creates some genuinely interesting behaviors that are worth understanding if you're trying to build a reliable Python supply chain.

What Yanking Actually Does

PEP 592 defines yanking as a two-bit state on a package release file: it's either yanked or it isn't, and if it is, an optional string explains why. The yanked state is exposed through the PyPI Simple API as a data-yanked attribute on each file link. When pip (or any installer implementing PEP 592) encounters a yanked file, the rule is specific: the installer will not select that file as part of normal resolution, but it will use it if — and only if — the version is explicitly pinned in the requirements.

In practical terms: pip install requests will skip yanked versions. pip install requests==2.28.0 where 2.28.0 happens to be yanked will still install 2.28.0, with a warning printed to the console. This is deliberate. Yanking is not "make this version disappear"; it's "stop new users from picking this up by default, but don't break anyone who's already pinned to it."

Why Yanking Exists

Before PEP 592, the only options for a maintainer who had shipped a broken release were to leave it in place or to delete it. Deletion on PyPI is permanent — once deleted, the version number is permanently burned and cannot be reused (this was a direct response to the left-pad incident on npm in March 2016, which showed the ecosystem damage caused by allowing unpublishes to republish). But deletion also breaks every lockfile, requirements.txt, and CI build in the world that pinned to the deleted version.

Yanking threads that needle. A broken release is quietly removed from new resolutions while existing installations remain reproducible. Maintainers get a recovery path that doesn't require either living with the bad release or breaking every user downstream of it.

The most common reasons a release gets yanked, based on a scan of yanked-reason strings across the top 1000 PyPI projects as of early 2024:

Critical bugs that slipped through testing ("broken on Python 3.12," "regression in X feature"). Accidentally shipped release artifacts ("missing files," "wrong Python version tag"). Security vulnerabilities that a patched version supersedes. Packaging errors ("broken wheel," "missing metadata"). Releases that should have been yanked instead of deleted but were initially deleted by mistake — this one less common now that yanking is well-understood.

The Security Angle

Yanking has specific security behavior that's worth understanding.

When a maintainer yanks a release because of a known vulnerability and publishes a fixed version, new users automatically get the fixed version. That's the happy path. But existing users pinned to the vulnerable version continue to install it, with only a warning. This is a deliberate choice — pinning is treated as a strong signal of intent — but it means yanking alone does not remediate a vulnerability in deployed systems.

For security-conscious projects, this has a practical consequence: a yanked version with known vulnerabilities is not automatically patched for consumers. You still need vulnerability scanning (pip-audit, safety, Dependabot, equivalent) to identify pinned versions that have become unsafe. Yanking is a hint to the resolver, not an announcement to the vulnerability database.

Conversely, yanking is sometimes used to quietly remove a release that has been found to contain malware. The PyPI admin team has the authority to yank releases under the PyPI Acceptable Use Policy, and in some cases they'll yank rather than delete so that existing reproducibility is preserved for forensic purposes. If you see an admin-yanked version with a reason string pointing to abuse or malware, treat any deployment of that version as compromised until proven otherwise.

When to Yank Versus Delete

For maintainers, the choice between yanking and deleting shows up any time a bad release hits PyPI. The practical rule is: yank almost always, delete almost never.

Yanking is the right answer for: broken code, regressions, packaging errors, unintended releases, releases superseded by a fix within hours. Deletion is the right answer for: releases that should never have existed (accidentally published test versions, pre-release code that leaked), releases containing credentials or other content that must be removed, cases where keeping the version reproducible would cause more harm than breaking pins.

Once a version is deleted, that version number is permanently reserved. You can't re-upload 1.2.3 after deleting it; you'd need to publish 1.2.4 or 1.2.3.post1. Yanking has no such restriction — a yanked version remains installable for pinned users forever.

The PyPI admins have been consistent in public guidance through 2023 and 2024 that yanking is the preferred tool. The pypi-announce mailing list archives have several examples of admins asking maintainers who requested a deletion whether yanking would solve the problem.

What Consumers Should Do

For consumers of PyPI packages, yanking creates a specific observability problem. Your lockfile might be pinned to a yanked version, you're still installing it, but you may not know unless you look. pip prints a warning at install time, but in a CI pipeline with verbose output, that warning is easy to miss.

Reasonable hygiene:

Run pip install --dry-run or equivalent regularly against your requirements to see warnings. Use tooling that explicitly checks for yanked dependencies — pip-audit does this since version 2.6 (released late 2023). Treat a yanked pinned dependency as a remediation item on your backlog, not an emergency, but also not something to ignore indefinitely. Review the yanked-reason string when you see one — "broken wheel" is very different from "security vulnerability."

The Long Tail

A side effect of PEP 592 is that yanked releases accumulate over time on PyPI. A popular project that has shipped for a decade may have hundreds of releases, a non-trivial fraction yanked. Those yanked releases still occupy storage, still have their files accessible, and still resolve for explicitly pinned users.

This isn't a problem from a storage-economics perspective (PyPI is generously supported by Fastly and other sponsors), but it does mean that the "history" of a package on PyPI includes releases the maintainer has actively repudiated. When you're auditing a dependency for security reasons — looking at its release cadence, its maintainer discipline, its response to incidents — the yank ratio is a real signal. A package with five yanked releases out of fifty is a project that has made mistakes and corrected them publicly, which is usually a good sign. A package with fifty yanked releases out of fifty-five is a different story.

How Safeguard Helps

Safeguard tracks the yank status of every PyPI release your projects depend on and surfaces yanked pinned versions in your findings feed, with the yanked-reason string and the recommended upgrade path. When a dependency yanks a version you're currently installing, Safeguard raises it as a supply-chain event and, where the yank is associated with a known CVE, correlates it with your vulnerability view. For teams publishing to PyPI, Safeguard monitors your own yank history as part of your publisher hygiene score, so a pattern of yanked releases shows up as a trend rather than individual incidents.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.