|
| 1 | +--- |
| 2 | +title: PyPI now serves project status markers in API responses |
| 3 | +description: > |
| 4 | + PyPI has implemented PEP 792, and is now serving |
| 5 | + project status markers in its standard HTML and JSON APIs. |
| 6 | +authors: |
| 7 | + - woodruffw |
| 8 | +date: 2025-08-14 |
| 9 | +tags: |
| 10 | + - security |
| 11 | + - integrations |
| 12 | +--- |
| 13 | + |
| 14 | +PyPI now serves [project status markers] in its standard |
| 15 | +[index APIs]. This allows downstream consumers (like Python package installers and |
| 16 | +index mirrors) to retrieve project statuses programmatically and use them to |
| 17 | +inform users when a project is archived or quarantined. |
| 18 | + |
| 19 | +## Summary |
| 20 | + |
| 21 | +* PyPI has implemented project status markers as proposed and accepted in |
| 22 | + [PEP 792]. |
| 23 | +* As of today, PyPI supports three standard statuses: **active** (the default), |
| 24 | + **archived**, and **quarantined**. |
| 25 | +* Downstream consumers can now retrieve these statuses via the standard index APIs |
| 26 | + and use them to inform users about the state of a project. |
| 27 | + |
| 28 | +See the [project archival] and [project quarantine] announcement posts |
| 29 | +for additional information on PyPI's implementation of those individual statuses. |
| 30 | + |
| 31 | +<!-- more --> |
| 32 | + |
| 33 | +## Background |
| 34 | + |
| 35 | +Many Python regularly find themselves asking the same |
| 36 | +questions again and again when evaluating a new dependency: |
| 37 | + |
| 38 | +* Is the dependency deprecated, potentially in favor of another project? |
| 39 | +* If a vulnerability is discovered in the dependency, is it likely to be |
| 40 | + patched? |
| 41 | +* Can I expect major future changes to the dependency, or is it |
| 42 | + "done" (i.e. feature complete)? |
| 43 | + |
| 44 | +These questions (and many others in the domain of supply chain security) |
| 45 | +essentially boil down to a single question: |
| 46 | +**what is the status of this project?** |
| 47 | + |
| 48 | +## The status quo before status markers |
| 49 | + |
| 50 | +Before [PEP 792], Python packaging had no less than three overlapping |
| 51 | +solutions for determining a project's status: |
| 52 | + |
| 53 | +* Individual releases of a project could include a `Development Status` |
| 54 | + [trove classifier] in their metadata, such as |
| 55 | + `Development Status :: 7 - Inactive` to indicate that the project is no |
| 56 | + longer actively maintained. |
| 57 | + |
| 58 | + However, trove classifiers occur at the _distribution_ level, leading |
| 59 | + to two problems: |
| 60 | + |
| 61 | + 1. To update a project's status, the project's maintainer must upload |
| 62 | + a new release with the updated classifier. This is unnecessarily |
| 63 | + onerous, particularly when the intent is to _stop_ updating the |
| 64 | + project! |
| 65 | + 2. Classifiers do not apply retroactively, meaning that all _previous_ |
| 66 | + releases of a project continue to have their original |
| 67 | + classifiers. This results in a misleading view of the project's status: |
| 68 | + a downstream that pins to `sampleproject==1.2.3` may fail to realize |
| 69 | + that `sampleproject===1.2.4` signals that the entire project is now |
| 70 | + inactive. |
| 71 | + |
| 72 | +* Indices can mark individual files (or entire releases) as "yanked," per the |
| 73 | + [file yanking specification]. Yanked files are effectively |
| 74 | + soft-deleted, meaning that they'll be skipped by complying installers |
| 75 | + during resolution but not if explicitly pinned by the user. |
| 76 | + |
| 77 | + Yanking is a useful tool for mitigating accidental vulnerabilities |
| 78 | + or compatibility breakages in a release, but it has the same "scope" |
| 79 | + issue as classifiers: it applies at the file and release level, |
| 80 | + not at the project level. |
| 81 | + |
| 82 | + Moreover, the semantics of yanking aren't appropriate for all potential |
| 83 | + statuses: soft deletion is still disruptive, whereas statuses like |
| 84 | + "archived" and "deprecated" suggest that the project is still |
| 85 | + suitable for installation, so long as the user can be made aware of |
| 86 | + its status. |
| 87 | + |
| 88 | +* PyPI itself has "project statuses," which apply to the entire project. |
| 89 | + These statuses were not standardized, and therefore only appeared |
| 90 | + on user-facing HTML pages, not in the standard APIs. This made |
| 91 | + them difficult to retrieve programmatically, limiting their usefulness. |
| 92 | + |
| 93 | +Beyond these partial solutions many downstreams also apply heuristics |
| 94 | +to determine a project's status, such as checking for recent project |
| 95 | +(or source repository) activity or using popularity metrics like |
| 96 | +GitHub stars as a proxy for project health. However, these heuristics |
| 97 | +can be manipulated or outright incorrect, such as when a project is feature |
| 98 | +complete and therefore has no recent activity. |
| 99 | + |
| 100 | +Overall, these partial solutions and heuristics point to a need for |
| 101 | +something better. |
| 102 | + |
| 103 | +## Project status markers |
| 104 | + |
| 105 | +That brings us to the new feature: project status markers. |
| 106 | + |
| 107 | +Project status markers are a Python packaging standard derived from PyPI's |
| 108 | +existing project statuses. The standard defines four project statuses, |
| 109 | +which have both index-side and installer-side semantics: |
| 110 | + |
| 111 | +* **active**: Indicates that the project is active. This is the default |
| 112 | + status, meaning that any project that does not explicitly |
| 113 | + declare a status is considered active. Active projects are |
| 114 | + not subject to any restrictions on upload or installation. |
| 115 | +* **archived**: Indicates that the project does not expect to be updated |
| 116 | + in the future. When a project is **archived**, PyPI will not allow |
| 117 | + new uploads to the project, and installers are encouraged to |
| 118 | + inform users about the project's archival. |
| 119 | +* **quarantined**: Indicates that the project is considered generally |
| 120 | + unsafe for use, e.g. due to malware. When a project is **quarantined**, PyPI |
| 121 | + will not offer it for installation, and installers are encouraged to |
| 122 | + produce a warning when users attempt to install it[^warning]. |
| 123 | +* **deprecated**: Indicates that the project is considered obsolete, |
| 124 | + and may have been superceded by another project. Unlike archived projects, |
| 125 | + deprecated projects can still be uploaded to, but installers are encouraged |
| 126 | + to inform users about the project's deprecation. |
| 127 | + |
| 128 | +Of these statuses, PyPI currently supports **active**, **archived**, and |
| 129 | +**quarantined**. PyPI doesn't support **deprecated** yet, but we'll be looking |
| 130 | +at supporting it now that the MVP is complete. |
| 131 | + |
| 132 | +Beyond the statuses themselves, the standard also defines an optional |
| 133 | +"status reason" that can be used to provide additional context about the status. |
| 134 | +PyPI doesn't currently expose status reasons, but may do so in the future. |
| 135 | + |
| 136 | +## Consuming status markers |
| 137 | + |
| 138 | +The standard is one thing, but let's see how to actually get status |
| 139 | +markers from PyPI's [index APIs]. |
| 140 | + |
| 141 | +Status markers are available in both the HTML and JSON index APIs. |
| 142 | +For the HTML API the `<meta>` fields are: |
| 143 | + |
| 144 | +* `pypi:project-status` for the project status itself (or **active** by default) |
| 145 | +* `pypi:project-status-reason` for the project status reason (if present) |
| 146 | + |
| 147 | +For example: |
| 148 | + |
| 149 | +```sh |
| 150 | +curl --silent \ |
| 151 | + -H "Accept: application/vnd.pypi.simple.v1+html" \ |
| 152 | + https://pypi.org/simple/pepy/ \ |
| 153 | + | htmlq --pretty 'head meta[name="pypi:project-status"]' |
| 154 | +``` |
| 155 | + |
| 156 | +Yields: |
| 157 | + |
| 158 | +```html |
| 159 | +<meta name="pypi:project-status" content="archived"> |
| 160 | +``` |
| 161 | + |
| 162 | +Within the JSON API, the project status is available via the |
| 163 | +top-level `project-status` object, which contains `status` and `reason` |
| 164 | +fields corresponding to the HTML API fields above. |
| 165 | + |
| 166 | +For example: |
| 167 | + |
| 168 | +```sh |
| 169 | +curl --silent \ |
| 170 | + -H "Accept: application/vnd.pypi.simple.v1+json" \ |
| 171 | + https://pypi.org/simple/pepy/ \ |
| 172 | + | jq '."project-status"' |
| 173 | +``` |
| 174 | + |
| 175 | +Yields: |
| 176 | + |
| 177 | +```json |
| 178 | +{ |
| 179 | + "status": "archived", |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +## Conclusion |
| 184 | + |
| 185 | +Starting today, Python package installers and other index consumers can |
| 186 | +retrieve status markers from PyPI's standard index APIs. |
| 187 | + |
| 188 | +Our hope is that downstreams *will* consume these markers, and use them |
| 189 | +as suggested by the standard. In particular we hope that installers like |
| 190 | +[pip] and [uv] will signal relevant statuses to users, helping them |
| 191 | +form a better picture of the status of their dependencies as well as set |
| 192 | +policies controlling which statuses are acceptable for installation. |
| 193 | + |
| 194 | +## Acknowledgements |
| 195 | + |
| 196 | +[PEP 792] was authored by [William Woodruff] (Astral) and |
| 197 | +[Facundo Tuesca] (Trail of Bits). We'd like to thank [Donald Stufft] for being |
| 198 | +the PEP's sponsor and PEP delegate. Additionally, we'd like to thank |
| 199 | +[Dustin Ingram] and [Mike Fiedler] for their review and feedback on the PEP |
| 200 | +and the associated changes to PyPI. |
| 201 | + |
| 202 | +The funding for this feature’s development comes in part from |
| 203 | +[Alpha-Omega]. Alpha-Omega’s mission is to protect society by catalyzing |
| 204 | +sustainable security improvements to the most critical open-source software |
| 205 | +projects and ecosystems. |
| 206 | + |
| 207 | +[project status markers]: https://packaging.python.org/en/latest/specifications/project-status-markers/ |
| 208 | +[index APIs]: https://docs.pypi.org/api/index-api/ |
| 209 | +[PEP 792]: https://peps.python.org/pep-0792/ |
| 210 | +[project archival]: 2025-01-30-archival.md |
| 211 | +[project quarantine]: 2024-12-30-quarantine.md |
| 212 | +[trove classifier]: https://pypi.org/classifiers |
| 213 | +[file yanking specification]: https://packaging.python.org/en/latest/specifications/file-yanking/ |
| 214 | +[Alpha-Omega]: https://alpha-omega.dev/ |
| 215 | +[Facundo Tuesca]: https://github.com/facutuesca |
| 216 | +[William Woodruff]: https://github.com/woodruffw |
| 217 | +[Donald Stufft]: https://github.com/dstufft |
| 218 | +[Dustin Ingram]: https://github.com/di |
| 219 | +[Mike Fiedler]: https://github.com/miketheman |
| 220 | +[pip]: https://pip.pypa.io/ |
| 221 | +[uv]: https://docs.astral.sh/uv/ |
| 222 | + |
| 223 | +[^warning]: This warning is technically moot, as PyPI itself will not offer |
| 224 | + any files from quarantined projects for installation. |
| 225 | + However, the warning can still help users understand _why_ |
| 226 | + their installation has failed, and is therefore recommended |
| 227 | + by the standard. |
0 commit comments