pypi
Use when finding Python packages, reading descriptions, checking version history, or getting pip install commands. Provides API and navigation flows for pypi.org.
PyPI — Python Package Index
Overview
Covers the full read workflow on PyPI: searching for packages, reading package detail pages (description, version history, download files), and retrieving metadata via the JSON API. The JSON API is the recommended approach for automated workflows — no CAPTCHA, no browser required.
Not covered here: Package upload/publishing, account management, user profiles, API tokens.
Auth required for: Uploading packages, managing releases. All browsing and search are public.
Agent Paths
Programmatic [API] — No browser needed
- Package metadata + all versions:
GET https://pypi.org/pypi/{name}/json - Specific version metadata:
GET https://pypi.org/pypi/{name}/{version}/json - Search: No clean JSON search API — use browser or navigate directly to
/project/{name}/
Required headers: None — all public endpoints are open
Browser [Browser] — Requires browser control
- Use selectors and Navigation Flows below
- Required when: searching without knowing the package name, browsing download files, reading classifiers/maintainer info
When to use which
- Default to Programmatic if you know the package name — one call returns everything
- Use Browser when: you need to search for a package by description (not name), or the task involves downloading/copying the pip install command
- Avoid browser search (
/search/) — hCaptcha blocks automated browsers. Use the JSON API instead.
Agent Knowledge Note
Most frontier LLMs have PyPI's API and package structure in training data. This skill adds value via:
- hCaptcha on
/search/— automated browsers are consistently blocked on the search results page. Training data may suggest using the search UI; the correct path is the JSON API or direct/project/{name}/navigation. - Tabs are preloaded, not lazy — Description, History, and Files tabs are all server-rendered in one HTML response. Tab click is CSS-only. Never wait for a network event after clicking a tab. This is the opposite of crates.io.
- Search results show no version number — The
.package-snippetelements contain the creation date, not the current version. To get the version, navigate to/project/{name}/or use the JSON API. robots.txtdisallows/search*— scraping the search page is against ToS; combined with hCaptcha, the JSON API is the correct path.
Site Map
| Page | URL Pattern | Notes |
|---|---|---|
| Homepage | https://pypi.org/ | Search form |
| Search results | https://pypi.org/search/?q={query} | hCaptcha-protected for bots — see Gotchas |
| Package detail | https://pypi.org/project/{name}/ | Description, history, files tabs |
| Specific version | https://pypi.org/project/{name}/{version}/ | Version-pinned detail |
| JSON API (latest) | https://pypi.org/pypi/{name}/json | Full metadata, no auth, no CAPTCHA |
| JSON API (version) | https://pypi.org/pypi/{name}/{version}/json | Specific version metadata |
Page Structures
Homepage
| Element | Selector | Notes |
|---|---|---|
| Search input | input[name="q"] | Placeholder: "Type '/' to search projects" |
| Search submit | button.search-form__button | |
| Nav: Log in | a[href^="/account/login/"] | |
| Nav: Register | a[href="/account/register/"] |
Package Detail (/project/{name}/)
| Element | Selector | Notes |
|---|---|---|
| Copy pip install | button.copy-tooltip | Copies pip install {name}==version to clipboard |
| Latest version badge | a.status-badge | Text: "Latest version" — shows "Yanked" for yanked releases |
| Description tab | #description-tab | aria-label="Project description..." |
| History tab | #history-tab | aria-label="Release history..." |
| Files tab | #files-tab | aria-label="Download files..." |
| Version history entries | a.card | Each card links to /project/{name}/{version}/ |
| Version number (in card) | a.card > p.release__version | Text: e.g. "2.32.5" — direct child <p> inside card |
| Release date (in card) | a.card > p.release__version-date time | datetime attr has ISO timestamp; text is humanized date |
| Maintainer links | a[href^="/user/"] | Links to each maintainer's profile |
| Classifier links | a[href^="/search/?c="] | Filter search by classifier (license, Python version, etc.) |
| Project homepage/source links | a.vertical-tabs__tab[href^="http"] | Homepage, docs, source in sidebar |
| Report malware | a[role="button"][href*="submit-malware-report"] |
Search Results (/search/?q={query})
| Element | Selector | Notes |
|---|---|---|
| Result items | .package-snippet | Each result is an <a> wrapping name + date + description |
| Result name | .package-snippet__name | Package name |
| Result creation date | .package-snippet__created | When the package was first published — not current version |
| Result description | .package-snippet__description | One-line summary |
| Pagination next | a[aria-label="Next page"] | Absent on last page |
| hCaptcha (if blocked) | button#capSubmit | Automated browsers may see this instead of results |
Note: Search results do not include the current version number. For current version, navigate to /project/{name}/ or use the JSON API.
Files Tab (within package detail, #files panel)
| Element | Selector | Notes |
|---|---|---|
| File name filter | input[name="bdist-filenames-filter"] | Filter wheel filenames |
| Interpreter filter | select[name="bdist-tags-interpreters"] | e.g. cp313, py3 |
| ABI filter | select[name="bdist-tags-abis"] | e.g. cp313, none |
| Platform filter | select[name="bdist-tags-platforms"] | e.g. win_amd64, any |
| Source dist link | a[href$=".tar.gz"] | Direct download URL |
| Wheel link | a[href$=".whl"] | Direct download URL |
Navigation Flows
Flow: Search for a Package [Browser]
Starting point: https://pypi.org/
Prerequisites: None
| Step | Action | Target | Value | Wait For | Timeout |
|---|---|---|---|---|---|
| 1 | navigate | — | https://pypi.org/ | input[name="q"] visible | 5s |
| 2 | type | input[name="q"] | Search term | — | — |
| 3 | press | input[name="q"] | Enter | URL contains /search/ | 3s |
Success state: URL is https://pypi.org/search/?q={term} with .package-snippet results visible.
Branching:
- If
button#capSubmitappears → hCaptcha is blocking — search not available for this browser session. Navigate directly to/project/{name}/if you know the package name, or use the JSON API:GET https://pypi.org/pypi/{name}/json
Flow: View Package Detail [Browser]
Starting point: Any page Prerequisites: None
| Step | Action | Target | Value | Wait For | Timeout |
|---|---|---|---|---|---|
| 1 | navigate | — | https://pypi.org/project/{name}/ | #description-tab visible | 5s |
| 2 | read | button.copy-tooltip, a.status-badge, page body | — | Description loaded | — |
Success state: Package page with description, version badge, maintainers, and sidebar links visible. Install command visible in header area.
Flow: Browse Version History [Browser]
Starting point: https://pypi.org/project/{name}/
Prerequisites: None
| Step | Action | Target | Wait For | Timeout |
|---|---|---|---|---|
| 1 | click | #history-tab | History panel visible | 1s |
| 2 | read | a.card | All version cards rendered | — |
To navigate to a specific version:
| Step | Action | Target | Wait For | Timeout |
|---|---|---|---|---|
| 3 | click | a.card:has-text("{version}") | Version detail page | 3s |
Success state: URL becomes https://pypi.org/project/{name}/{version}/ with that version's metadata. Status badge may read "Latest version" or "Yanked".
Flow: Get Package Metadata [API]
Starting point: None — direct HTTP fetch Prerequisites: None
| Step | Action | Target | Notes |
|---|---|---|---|
| 1 | fetch | https://pypi.org/pypi/{name}/json | Returns full metadata as JSON |
| 2 | read | info.name, info.version, info.summary | Core fields |
Success state: JSON with top-level keys info, releases, urls, vulnerabilities.
API Response Shapes
GET /pypi/{name}/json
{
"info": {
"name": "requests",
"version": "2.32.3",
"summary": "Python HTTP for Humans.",
"requires_python": ">=3.8",
"home_page": "https://requests.readthedocs.io",
"license": "Apache-2.0",
"author": "Kenneth Reitz",
"author_email": "[email protected]",
"requires_dist": [
"charset-normalizer<4,>=2",
"idna<4,>=2.5",
"urllib3<3,>=1.21.1",
"certifi>=2017.4.17"
],
"classifiers": [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.11"
],
"project_urls": {
"Homepage": "https://requests.readthedocs.io",
"Source": "https://github.com/psf/requests"
}
},
"releases": {
"2.32.3": [
{
"filename": "requests-2.32.3-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/.../requests-2.32.3-py3-none-any.whl",
"size": 64928,
"upload_time_iso_8601": "2024-05-29T15:37:05.856790Z",
"requires_python": ">=3.8",
"yanked": false,
"yanked_reason": null
}
]
},
"urls": [
{
"filename": "requests-2.32.3-py3-none-any.whl",
"url": "https://files.pythonhosted.org/packages/...",
"packagetype": "bdist_wheel"
}
],
"vulnerabilities": []
}
Key fields:
| Field | Description |
|---|---|
info.version | Latest version string |
info.requires_python | Python version constraint |
info.requires_dist[] | PEP 508 dependency strings |
releases | Object keyed by version string → array of distribution files |
releases[ver][].yanked | true means this specific file is yanked |
urls[] | Download URLs for latest version |
vulnerabilities[] | Known CVEs from OSV database — empty array = no known vulns |
Common Agent Tasks
"What version of {package} should I install?"
- Fetch:
GET https://pypi.org/pypi/{name}/json - Read
info.version— this is the latest stable version - Read
info.requires_python— confirm compatibility with user's Python version - Read
vulnerabilities[]— if non-empty, mention known CVEs - Output:
pip install {name}=={version}and any compatibility caveats
"Is {package} actively maintained?"
- Fetch:
GET https://pypi.org/pypi/{name}/json - Read
releaseskeys — count total releases; sort and get the 3 most recent - Read
releases[latest][0].upload_time_iso_8601— when was the last release? - Read
info.home_pageandproject_urls.Source— is there a linked repo? - Read
vulnerabilities[]— any unpatched CVEs? - Output: latest release date, total release count, source link, vulnerability count
"Compare {pkg1} and {pkg2}"
- Fetch both in parallel:
GET /pypi/{pkg1}/jsonandGET /pypi/{pkg2}/json - Compare:
info.version,info.requires_python,info.license - Compare dependency count:
len(info.requires_dist) - Compare release history:
len(releases)and recency of latest release - Compare vulnerability status:
len(vulnerabilities)for each - Output: side-by-side table of version / Python req / license / dep count / release count / last release / vuln count
Dynamic Content Patterns
- MPA with server-rendered HTML: No JS framework. Every page navigation is a full page load. Use
domcontentloaded. - Tabs are already in DOM: All three tab panels (description, history, files) are server-rendered on the same page. Clicking a tab toggles CSS visibility only — no network request, no waiting.
- Version history is fully inline: All releases are listed in the
#historypanel with no pagination, even for packages with 100+ versions. - hCaptcha on /search/: Automated headless browsers are frequently challenged by hCaptcha on the search endpoint. Direct package URLs (
/project/{name}/) and the JSON API are unaffected. - Yanked releases: Appear in history with " yanked" in the card text. pip will refuse to install them unless the version is pinned exactly.
Timing & Waiting
| Transition | Wait For | Typical Delay |
|---|---|---|
| Page navigation | domcontentloaded | 0.5–2s |
| Tab click (description/history/files) | No wait — content already in DOM | <100ms |
| Search submit | URL changes to /search/ | 1–3s |
Rate Limits
| Scope | Limit | Notes |
|---|---|---|
| JSON API | No documented rate limit | Requests are served by CDN; generally very permissive |
Browser /search/ | hCaptcha-gated | Automated browsers are blocked; use API instead |
| Download files | Bandwidth throttling on very large downloads | Not relevant for metadata tasks |
Parallel requests: Safe to fire multiple metadata API requests in parallel — the JSON API is CDN-backed and handles high concurrency.
Common Gotchas
-
hCaptcha blocks automated search —
/search/is blocked for headless browsers. Detect viabutton#capSubmit. Workaround: use the JSON API (https://pypi.org/pypi/{name}/json) or navigate directly to/project/{name}/if you know the package name. -
Tabs don't trigger network requests — Description, history, and files content are all server-rendered in one HTML response. Clicking
#history-tabjust toggles visibility. Never wait for a network event after tab click. -
Package name canonicalization — PyPI treats hyphens and underscores as equivalent, and names are case-insensitive.
pip_tools,pip-tools, andPip-Toolsall go to the same page. The canonical name is ininfo.namefrom the JSON API. -
Yanked releases still appear in history — Yanked versions stay visible in
a.cardentries with "yanked" in the text. They won't install via pip without explicit version pinning. -
robots.txtdisallows/search*— Well-behaved crawlers should not scrape search. Combined with hCaptcha, the intended path for programmatic lookup is the JSON API. -
Version history shows all releases — No pagination on the history tab.
document.querySelectorAll('a.card').lengthgives the total release count. -
History entries from JSON API —
releaseskey in the JSON API response is an object keyed by version string. IterateObject.keys(data.releases).reverse()for chronological order.
Anti-Patterns
- NEVER use the browser search page for automation — hCaptcha blocks headless browsers on
/search/. UseGET /pypi/{name}/jsonor navigate directly to/project/{name}/. - NEVER wait for a network event after clicking a tab — tabs are preloaded in the HTML. Waiting for
networkidleorloadafter a tab click will timeout or proceed immediately with wrong content. - NEVER expect version number in search results —
.package-snippetelements show the creation date (.package-snippet__created), not the current version. There is no current version in search results — go to the detail page or API. - NEVER use
.card__titleto extract version — the version text is ina.card > p.release__version(direct child<p>)..card__titledoes not exist. - NEVER assume
releasesis an array —releasesis an object keyed by version string, not an array. IterateObject.keys(data.releases).
Freshness
| Skill verified | 2026-02-20 |
| API stable since | 2018 — PyPI JSON API (PEP 503 + warehouse); breaking changes are extremely rare |
| Selector stability | HIGH — PyPI uses intentional class names (.package-snippet, a.card, .copy-tooltip); no hashing |
| Breaking change risk | Low — PyPI is very conservative about UI changes |