PyPI Stats
App · Analytics · 6 actions
Read PyPI download statistics from a workflow. Get a package's downloads over the last day, week and month, or its full daily series over the 180 days PyPI Stats keeps, with the total, the average per day, the busiest day and how the window compares with the one before it. Split those downloads by Python major version, by minor version to see whether 3.9 can be dropped, or by operating system to see which wheels are worth building. Compare several packages side by side, ranked with each one's share. Mirror traffic is excluded by default, so the figures are real installs. Every action reads: nothing here changes anything. No account or API key required.
Actions
| Action | What it does |
|---|---|
| Compare packages | Compare the recent downloads of several packages side by side, ranked biggest first with each one's share of the total. |
| Get download history | Get a package's daily download series, up to the 180 days PyPI Stats keeps. |
| Get downloads by operating system | Split a package's downloads by the operating system that asked for them: Windows, Linux, macOS (which PyPI records as Darwin), anything else, and the downloads that did not say. |
| Get downloads by Python major version | Split a package's downloads by Python major version: how many came from Python 3, how many from Python 2, and how many from downloads that did not say which. |
| Get downloads by Python minor version | Split a package's downloads by Python minor version: 3.13, 3.12, 3.11 and so on, each with its total and its share of the window. |
| Get recent downloads | Get how many times a package was downloaded from PyPI in the last day, the last week and the last month. |
Compare packages
Compare the recent downloads of several packages side by side, ranked biggest first with each one's share of the total. Answers 'are we still ahead of them?' in one step rather than several. Between two and ten packages; a package PyPI Stats has no figures for is listed as missing rather than failing the step. No API key required.
Inputs
- Packages
packageslistRequired - The packages to compare, between 2 and 10 of them. Spell them however you like: the same package named twice is only fetched once.
- Compare over
periodtext - Which window the ranking uses. Each package's last day, week and month are all returned whichever you pick.One of:
day(The last day),week(The last week),month(The last month). Default:month.
Outputs
foundtrue/falseleadertextmissinglistpackage_countnumberpackageslistperiodtexttotal_downloadsnumber
Example output
{
"found": true,
"leader": "requests",
"missing": [],
"package_count": 3,
"packages": [
{
"downloads": 1284930112,
"last_day": 42118904,
"last_month": 1284930112,
"last_week": 296443118,
"package": "requests",
"percent": 71.22,
"rank": 1,
"url": "https://pypistats.org/packages/requests"
},
{
"downloads": 401118004,
"last_day": 13204118,
"last_month": 401118004,
"last_week": 92884001,
"package": "httpx",
"percent": 22.23,
"rank": 2,
"url": "https://pypistats.org/packages/httpx"
},
{
"downloads": 118070339,
"last_day": 3884112,
"last_month": 118070339,
"last_week": 27118004,
"package": "aiohttp",
"percent": 6.54,
"rank": 3,
"url": "https://pypistats.org/packages/aiohttp"
}
],
"period": "month",
"total_downloads": 1804118455
}Get download history
Get a package's daily download series, up to the 180 days PyPI Stats keeps. Comes back with the total, the average per day, the busiest day, and the same figure for the equal window immediately before, so a step can say 'up 12% on the month before' rather than only quoting a number. Roll the rows up by week or month for a chart, or ask for no series at all when only the totals matter. Mirror traffic is left out by default. No API key required.
Inputs
- Package
packagetextRequired - The PyPI package to read. Spell it however you like: Zope.Interface, zope_interface and zope-interface all reach the same package. Use __all__ for the totals across the whole of PyPI.
- Mirror traffic
mirrorstext - Mirrors such as bandersnatch download every package on PyPI whether anyone uses it or not, so they are left out by default. Counting both keeps them on separate rows; the total then counts the with-mirrors figure once rather than adding it to a subset of itself.One of:
exclude(Real users only: leave mirror traffic out),include(Everything: count mirror traffic too),both(Both, kept apart so you can see the difference). Default:exclude. - From
start_datedate - Only count downloads on or after this date. Leave empty to start at the earliest day PyPI Stats still holds, which is 180 days ago.
- To
end_datedate - Only count downloads on or before this date. Leave empty to run up to the most recent day counted.
- Roll the series up by
granularitytext - How much detail the returned series carries. Daily is one row per day, which is 180 rows over the full window; weekly and monthly roll those up; 'No series' returns the totals and the per-category split without any rows at all.One of:
daily(Daily: one row per day),weekly(Weekly: rows start on a Monday),monthly(Monthly: one row per calendar month),none(No series: just the totals). Default:daily.
Outputs
average_per_daynumberbusiest_dayobjectcategorieslistdata_availableobjectdate_rangeobjectfoundtrue/falsegranularitytextlast_updatedtextmirrorstextpackagetextprevious_periodobjectserieslisttotal_downloadsnumberurltext
Example output
{
"average_per_day": 42831003.73,
"busiest_day": {
"date": "2026-08-11",
"downloads": 51204884
},
"categories": [
{
"category": "without_mirrors",
"downloads": 1284930112,
"label": "Excluding mirrors",
"percent": 100.0
}
],
"data_available": {
"days": 180,
"first_date": "2026-02-25",
"last_date": "2026-08-23",
"retention_days": 180
},
"date_range": {
"days": 30,
"first_date": "2026-07-25",
"last_date": "2026-08-23"
},
"found": true,
"granularity": "daily",
"last_updated": "2026-08-23",
"mirrors": "exclude",
"package": "requests",
"previous_period": {
"change": 80811657,
"change_percent": 6.71,
"comparable": true,
"downloads": 1204118455,
"end_date": "2026-07-24",
"start_date": "2026-06-25"
},
"series": [
{
"date": "2026-08-22",
"downloads": 41882901
},
{
"date": "2026-08-23",
"downloads": 42118904
}
],
"total_downloads": 1284930112,
"url": "https://pypistats.org/packages/requests"
}Get downloads by operating system
Split a package's downloads by the operating system that asked for them: Windows, Linux, macOS (which PyPI records as Darwin), anything else, and the downloads that did not say. Each comes back with its total and its share, plus a daily series you can roll up by week or month. The step for deciding which wheels are worth building. No API key required.
Inputs
- Package
packagetextRequired - The PyPI package to read. Spell it however you like: Zope.Interface, zope_interface and zope-interface all reach the same package. Use __all__ for the totals across the whole of PyPI.
- Operating systems
systemslist - Narrow the answer to particular operating systems. Leave empty for all of them, which is usually what you want since the shares only mean something against the whole.One or more of:
Windows,Linux,Darwin(macOS (recorded as Darwin)),other(Other: anything else the download named),null(Unknown: the download did not say). - From
start_datedate - Only count downloads on or after this date. Leave empty to start at the earliest day PyPI Stats still holds, which is 180 days ago.
- To
end_datedate - Only count downloads on or before this date. Leave empty to run up to the most recent day counted.
- Roll the series up by
granularitytext - How much detail the returned series carries. Daily is one row per day, which is 180 rows over the full window; weekly and monthly roll those up; 'No series' returns the totals and the per-category split without any rows at all.One of:
daily(Daily: one row per day),weekly(Weekly: rows start on a Monday),monthly(Monthly: one row per calendar month),none(No series: just the totals). Default:daily.
Outputs
average_per_daynumberbusiest_dayobjectcategorieslistdata_availableobjectdate_rangeobjectfoundtrue/falsegranularitytextlast_updatedtextpackagetextprevious_periodobjectserieslisttotal_downloadsnumberurltext
Example output
{
"average_per_day": 42831003.73,
"busiest_day": {
"date": "2026-08-11",
"downloads": 51204884
},
"categories": [
{
"category": "Linux",
"downloads": 1102884001,
"label": "Linux",
"percent": 85.83
},
{
"category": "Windows",
"downloads": 92118004,
"label": "Windows",
"percent": 7.17
},
{
"category": "Darwin",
"downloads": 61003118,
"label": "macOS",
"percent": 4.75
},
{
"category": "null",
"downloads": 28924989,
"label": "Unknown",
"percent": 2.25
}
],
"data_available": {
"days": 180,
"first_date": "2026-02-25",
"last_date": "2026-08-23",
"retention_days": 180
},
"date_range": {
"days": 30,
"first_date": "2026-07-25",
"last_date": "2026-08-23"
},
"found": true,
"granularity": "daily",
"last_updated": "2026-08-23",
"package": "requests",
"previous_period": {
"change": 80811657,
"change_percent": 6.71,
"comparable": true,
"downloads": 1204118455,
"end_date": "2026-07-24",
"start_date": "2026-06-25"
},
"series": [
{
"category": "Linux",
"date": "2026-08-23",
"downloads": 36118004,
"label": "Linux"
},
{
"category": "Windows",
"date": "2026-08-23",
"downloads": 3011884,
"label": "Windows"
}
],
"total_downloads": 1284930112,
"url": "https://pypistats.org/packages/requests"
}Get downloads by Python major version
Split a package's downloads by Python major version: how many came from Python 3, how many from Python 2, and how many from downloads that did not say which. Each category comes back with its total and its share, plus a daily series you can roll up by week or month. The step for 'is anyone still on Python 2?'. No API key required.
Inputs
- Package
packagetextRequired - The PyPI package to read. Spell it however you like: Zope.Interface, zope_interface and zope-interface all reach the same package. Use __all__ for the totals across the whole of PyPI.
- Python versions
versionslist - Narrow the answer to particular major versions. Leave empty for all of them, which is usually what you want since the shares only mean something against the whole.One or more of:
3(Python 3),2(Python 2),null(Unknown: the download did not say). - From
start_datedate - Only count downloads on or after this date. Leave empty to start at the earliest day PyPI Stats still holds, which is 180 days ago.
- To
end_datedate - Only count downloads on or before this date. Leave empty to run up to the most recent day counted.
- Roll the series up by
granularitytext - How much detail the returned series carries. Daily is one row per day, which is 180 rows over the full window; weekly and monthly roll those up; 'No series' returns the totals and the per-category split without any rows at all.One of:
daily(Daily: one row per day),weekly(Weekly: rows start on a Monday),monthly(Monthly: one row per calendar month),none(No series: just the totals). Default:daily.
Outputs
average_per_daynumberbusiest_dayobjectcategorieslistdata_availableobjectdate_rangeobjectfoundtrue/falsegranularitytextlast_updatedtextpackagetextprevious_periodobjectserieslisttotal_downloadsnumberurltext
Example output
{
"average_per_day": 42831003.73,
"busiest_day": {
"date": "2026-08-11",
"downloads": 51204884
},
"categories": [
{
"category": "3",
"downloads": 1281003118,
"label": "Python 3",
"percent": 99.69
},
{
"category": "2",
"downloads": 3836882,
"label": "Python 2",
"percent": 0.3
},
{
"category": "null",
"downloads": 90112,
"label": "Unknown",
"percent": 0.01
}
],
"data_available": {
"days": 180,
"first_date": "2026-02-25",
"last_date": "2026-08-23",
"retention_days": 180
},
"date_range": {
"days": 30,
"first_date": "2026-07-25",
"last_date": "2026-08-23"
},
"found": true,
"granularity": "daily",
"last_updated": "2026-08-23",
"package": "requests",
"previous_period": {
"change": 80811657,
"change_percent": 6.71,
"comparable": true,
"downloads": 1204118455,
"end_date": "2026-07-24",
"start_date": "2026-06-25"
},
"series": [
{
"category": "3",
"date": "2026-08-23",
"downloads": 41993118,
"label": "Python 3"
},
{
"category": "2",
"date": "2026-08-23",
"downloads": 122884,
"label": "Python 2"
}
],
"total_downloads": 1284930112,
"url": "https://pypistats.org/packages/requests"
}Get downloads by Python minor version
Split a package's downloads by Python minor version: 3.13, 3.12, 3.11 and so on, each with its total and its share of the window. This is the step that answers 'can we drop support for 3.9 yet?' with a number instead of a guess. Comes with a daily series you can roll up by week or month. No API key required.
Inputs
- Package
packagetextRequired - The PyPI package to read. Spell it however you like: Zope.Interface, zope_interface and zope-interface all reach the same package. Use __all__ for the totals across the whole of PyPI.
- Python versions
versionslist - Narrow the answer to particular minor versions, written as a major and a minor number: 3.12, 3.11. Use null for downloads that did not say which Python they were for. Leave empty for every version.
- From
start_datedate - Only count downloads on or after this date. Leave empty to start at the earliest day PyPI Stats still holds, which is 180 days ago.
- To
end_datedate - Only count downloads on or before this date. Leave empty to run up to the most recent day counted.
- Roll the series up by
granularitytext - How much detail the returned series carries. Daily is one row per day, which is 180 rows over the full window; weekly and monthly roll those up; 'No series' returns the totals and the per-category split without any rows at all.One of:
daily(Daily: one row per day),weekly(Weekly: rows start on a Monday),monthly(Monthly: one row per calendar month),none(No series: just the totals). Default:daily.
Outputs
average_per_daynumberbusiest_dayobjectcategorieslistdata_availableobjectdate_rangeobjectfoundtrue/falsegranularitytextlast_updatedtextpackagetextprevious_periodobjectserieslisttotal_downloadsnumberurltext
Example output
{
"average_per_day": 42831003.73,
"busiest_day": {
"date": "2026-08-11",
"downloads": 51204884
},
"categories": [
{
"category": "3.12",
"downloads": 402118332,
"label": "3.12",
"percent": 31.29
},
{
"category": "3.11",
"downloads": 331209884,
"label": "3.11",
"percent": 25.78
},
{
"category": "null",
"downloads": 90112,
"label": "Unknown",
"percent": 0.01
}
],
"data_available": {
"days": 180,
"first_date": "2026-02-25",
"last_date": "2026-08-23",
"retention_days": 180
},
"date_range": {
"days": 30,
"first_date": "2026-07-25",
"last_date": "2026-08-23"
},
"found": true,
"granularity": "daily",
"last_updated": "2026-08-23",
"package": "requests",
"previous_period": {
"change": 80811657,
"change_percent": 6.71,
"comparable": true,
"downloads": 1204118455,
"end_date": "2026-07-24",
"start_date": "2026-06-25"
},
"series": [
{
"category": "3.12",
"date": "2026-08-23",
"downloads": 13920114,
"label": "3.12"
},
{
"category": "3.11",
"date": "2026-08-23",
"downloads": 11002884,
"label": "3.11"
}
],
"total_downloads": 1284930112,
"url": "https://pypistats.org/packages/requests"
}Get recent downloads
Get how many times a package was downloaded from PyPI in the last day, the last week and the last month. All three numbers come back every time; the period you choose picks which one lands in the single 'downloads' field, so a threshold or an alert has one number to read. Mirror traffic is already excluded, so these are real installs. The starting point for 'how is my package doing?'. No API key required.
Inputs
- Package
packagetextRequired - The PyPI package to read. Spell it however you like: Zope.Interface, zope_interface and zope-interface all reach the same package. Use __all__ for the totals across the whole of PyPI.
- Headline period
periodtext - Which window the single 'downloads' figure covers. The last day, week and month are all returned whichever you pick.One of:
day(The last day),week(The last week),month(The last month). Default:month.
Outputs
downloadsnumberfoundtrue/falselast_daynumberlast_monthnumberlast_weeknumberpackagetextperiodtexturltext
Example output
{
"downloads": 1284930112,
"found": true,
"last_day": 42118904,
"last_month": 1284930112,
"last_week": 296443118,
"package": "requests",
"period": "month",
"url": "https://pypistats.org/packages/requests"
}