Treat PyPI project metadata as a claim, not proof. PyPI's own documentation says the metadata comes from values provided at upload time and does not necessarily match the content of the uploaded files. After an incident, check the files and their hashes, not only the description page.
What the documentation says
The PyPI JSON API documentation describes a route that returns metadata about a project, a list of releases and project URLs. It adds a point worth reading slowly: the returned metadata comes from the values provided at upload time and does not necessarily match the content of the uploaded files. It also says the first uploaded data for a release is stored, and later uploads do not update it.
That is not a flaw. It is how package metadata works. It does mean the author, links, summary and dependency lists on a project page reflect what the uploader declared.
Why this matters in a news cycle
Suppose a story says a package was altered. You open its PyPI page and everything looks normal: a familiar author name, a link to the right repository, a clean description. That tells you the uploader typed those values. It does not tell you the wheel or sdist contains what the repository contains.
Likewise, a project page that looks changed may only mean someone edited the declared values.
What the JSON API gives you for verification
The same documentation says each release entry includes hash digests, MD5, SHA256 and BLAKE2b-256, keyed by release version. The file objects also carry upload time fields. Those are much more useful for checking evidence.
curl -s https://pypi.org/pypi/some-project/json
From the output, record for the version in question: the filename, the SHA256 digest and the upload time. Then compare to what you installed.
A practical check
- Find the version you run, from
pip freezeor the lockfile. - Get the published SHA256 for that file from the JSON API.
- If you use a hash pinned requirements file or a lock with hashes, compare against it.
- For a suspicious version, download it without installing, unpack it and compare against the prior version. See the post on diffing a suspicious release.
- Record the digest, the upload time and the result in the ticket.
A matching digest tells you that your file is the same as the one the index published. It does not tell you the file is benign. For that you need analysis.
Other fields to read with care
- The yanked flag and yanked reason appear in the info block. A yank is a maintainer signal, not proof of an incident.
- Deprecated keys are called out in the docs: downloads is always minus one, so do not use PyPI JSON as a download counter.
- The project URLs field is declared by the uploader, so a link to a repository is a claim until you check the repository.
What to say in your incident notes
Say "metadata as declared at upload" when you cite author or links. Say "published digest" when you cite hashes. Keep those separate from your own analysis of the contents.
A worked example
Imagine a report says a Python package called some-project had a malicious release. You use it. You open the PyPI project page and everything looks reassuring: the author name matches the one you remember, the homepage link points to the right repository, and the description is unchanged.
That reassurance is exactly what the documentation warns about. Metadata is declared at upload time. A tampered release can carry familiar looking metadata.
So you do this instead.
- You pull the project record from the JSON API and save it to the ticket.
- You find the version your lockfile pins, 2.3.1, and note its file names, SHA256 digests and upload times.
- You compare the digest in your hash pinned requirements file with the published digest. They match, so your install is the same file the index published for 2.3.1.
- The report names 2.3.2 as the bad release. Your upload time check shows 2.3.2 was uploaded after your last build, so the build could not have fetched it.
- You record all of this and close the item, with the evidence attached.
Notice what you did not need: the project page's description, author field or links. You used the digests and the timestamps.
Which fields to trust for what
| Field | What it really is | Use it for |
|---|---|---|
| Author, summary, links | Values declared at upload time | Context only, never proof |
| File digests | Hashes of the published files | Comparing against what you installed |
| Upload time | When a file was uploaded | Building the exposure window |
| Yanked flag and reason | A maintainer signal | A hint to read more, not a verdict |
| Vulnerabilities array | Known issues for the release the route describes | A starting point, check the version specific route |
| Downloads | Deprecated, always minus one | Nothing |
The documentation also says the vulnerabilities array on the project route describes the most recent release, and that the release specific route gives precise control. If you care about a particular version, ask for that version.
Limits of a matching digest
A matching digest shows that two files are identical. It cannot tell you whether the file is harmless. If a malicious file is what the index published, and that is what you installed, the digests match. So digests answer "is my copy the same as the published one" and not "is it safe."
For the second question you need content analysis, such as comparing against the previous release or reading the code. See the post on diffing a suspicious release.
What to write in your notes
Be precise about the source of each statement. "Author listed as X on the project page" is a statement about declared metadata. "SHA256 of our installed wheel matches the index record" is a statement about files. "No install hooks found in the diff" is a statement about your analysis. Mixing them into "the package looks fine" hides the difference, and the difference is the whole lesson.
A habit worth keeping
Whenever you cite a project page as evidence in a ticket, ask what exactly the page can prove. If the answer is "only what the uploader said", cite it as that and look for a second kind of evidence.
Try it yourself
Click through it at your own pace, or open it full screen. Look at how every component and version gets an entry, which is the list you compare digests against.
Quick check
Does a normal looking PyPI page mean the release is clean?
No. The page shows values declared at upload time, which may not match the file contents.
Can I use the JSON API to count downloads?
No. The docs list the downloads key as deprecated and always minus one.
Is the project description on PyPI ever useful in an incident?
Yes, as context, such as the declared repository link. Treat it as a claim to check, not as evidence about the files.
Sources
Last reviewed: 19 October 2026.