Unofficial Python library and CLI for reading public data from the Google Play Store: autocomplete, search results, and per-app details. No API keys, no auth.
Project description
google-play-api-unofficial
A small Python library and CLI for reading public data from the Google Play Store. No API keys, no auth, no third-party services. Works as a command-line tool for quick lookups, or as an importable library for scripts and pipelines.
Unofficial — not affiliated with or endorsed by Google.
It covers three levels of detail:
- Autocomplete — what the Play Store search box would suggest for a half-query
- Search results — the top 30 apps matching a query (title, rating, installs, etc.)
- App details — full metadata for one app (description, release date, reviews, install counts, developer contact, screenshots)
Install
git clone https://github.com/tejmagar/google-play-api-unofficial
cd google-play-api-unofficial
python -m venv venv
source venv/bin/activate
pip install -e .
After install, the google-play-api-unofficial command is on your PATH. Python 3.9+, zero runtime dependencies (stdlib only).
Table of contents
Quick start (CLI)
google-play-api-unofficial suggest vpn
google-play-api-unofficial search "habit tracker"
google-play-api-unofficial search "habit tracker" --with-details
google-play-api-unofficial details com.duolingo
google-play-api-unofficial all "vpn" "habit tracker"
Add --json to any command for machine-readable output.
CLI reference
The command has four subcommands plus global flags.
suggest — autocomplete
Fetches what the Play Store search box would suggest for a half-query. Useful for finding related terms and similar apps.
google-play-api-unofficial suggest vpn
google-play-api-unofficial suggest "habit"
google-play-api-unofficial suggest "puzzle" --filter games
google-play-api-unofficial suggest vpn "habit" workout --json
Use --filter to include games or all types (default: apps).
Output:
=== vpn ===
Suggestions (5):
- vpn
- vpn and proxy tools
- vpn 1111
- vpn india
- vpnify
search — top 30 apps
Fetches the top 30 apps matching a query. Each result has: title, package, rating, category, developer, installs, icon, url.
google-play-api-unofficial search "habit tracker"
google-play-api-unofficial search vpn "habit tracker" --json
Output:
=== habit tracker ===
Apps (30):
- Loop Habit Tracker 4.6* 5,000,000+ [Productivity]
org.isoron.uhabits
- Disciplined - Habit Tracker 4.6* 500,000+ [Productivity]
app.disciplined.productive.structured.habit.tracker
...
You can pass multiple queries — they're run sequentially:
google-play-api-unofficial search "habit tracker" "daily habit" "routine planner"
search --with-details
Enriches every result with the full details payload (description, release date, reviews count, etc.). Slower — one extra request per app, 200ms between requests to avoid 429s.
google-play-api-unofficial search "habit tracker" --with-details
Output (per app, indented under "Apps"):
=== habit tracker ===
Apps (30):
Title: Loop Habit Tracker
Package: org.isoron.uhabits
Score: 4.6 (43,000 ratings, 2,200 reviews)
Installs: 5,000,000+
Released: Jan 15, 2016
Updated: May 12, 2026
...
Description:
A beautiful, open source habit tracker...
details — rich per-app data
Fetches description, release date, last updated, score, ratings count, reviews count, star histogram, install count (with real number), content rating, developer + id, developer website/email, IAP price range, and all screenshot URLs.
google-play-api-unofficial details com.duolingo
google-play-api-unofficial details ch.protonvpn.android com.nordvpn.android com.duolingo
google-play-api-unofficial details com.duolingo --json
Multiple packages are fetched sequentially with 200ms between requests.
all — suggest + search
Runs both suggest and search for one or more queries.
google-play-api-unofficial all "habit tracker"
google-play-api-unofficial all vpn "habit" --json
Output:
=== vpn ===
Suggestions (5):
- vpn
- vpn and proxy tools
...
Apps (30):
- Turbo VPN - Secure VPN Proxy ...
free.vpn.unblock.proxy.turbovpn
...
Global flags
| Flag | Applies to | Effect |
|---|---|---|
--json |
all | Output JSON instead of formatted text |
--filter {apps,games,all} |
suggest |
Restrict the type of apps included (default: apps) |
--with-details |
search |
Enrich each result with full details |
JSON output shapes
suggest:
{ "vpn": ["vpn", "vpn and proxy tools", "vpn 1111"] }
search:
{
"habit tracker": {
"apps": [
{
"package": "org.isoron.uhabits",
"title": "Loop Habit Tracker",
"rating": "4.6",
"category": "Productivity",
"developer": "Alkaline Software",
"installs": "5,000,000+",
"icon": "https://play-lh.googleusercontent.com/...",
"url": "https://play.google.com/store/apps/details?id=org.isoron.uhabits"
}
]
}
}
details:
{
"com.duolingo": {
"package": "com.duolingo",
"title": "Duolingo: Language Lessons",
"score": "4.7",
"ratings_count": "45,891,296",
"reviews_count": "943,469",
"histogram": [[5, 949035], [4, 380492], [3, 1042115], [2, 5598924], [1, 37920696]],
"installs": "500,000,000+",
"installs_min": 500000000,
"installs_real": 919061546,
"released": "May 29, 2013",
"updated": "Jun 2, 2026",
"content_rating": "Everyone",
"developer": "Duolingo",
"developer_id": "6957685454452609502",
"developer_email": "super-support@duolingo.com",
"developer_website": "https://www.duolingo.com/help/support-request",
"iap_range": "$0.99 - $239.99 per item",
"short_description": "Lessons to learn Spanish, French, German, English, Online Chess, Math & Music",
"description": "Learn a new language, chess & more with the world's most downloaded education app!...",
"icon": "https://play-lh.googleusercontent.com/...",
"screenshots": ["https://play-lh.googleusercontent.com/...", "..."],
"url": "https://play.google.com/store/apps/details?id=com.duolingo"
}
}
all:
{
"vpn": {
"suggestions": ["vpn", "vpn and proxy tools"],
"apps": [{ "package": "...", "title": "...", "rating": "4.6" }]
}
}
Programmatic usage
The same three functions are importable as a library.
Programmatic quick start
from google_play_api_unofficial import fetch_suggestions, fetch_apps, fetch_app_details
# Autocomplete
sugs = fetch_suggestions("vpn")
# -> ["vpn", "vpn and proxy tools", "vpn 1111", "vpn india", "vpnify"]
# Top 30 apps
apps = fetch_apps("habit tracker")
# -> [{"package": "org.isoron.uhabits", "title": "Loop Habit Tracker", ...}, ...]
# Full details for one app
d = fetch_app_details("com.duolingo")
# -> {"package": "com.duolingo", "title": "Duolingo: ...", "score": "4.7", ...}
All three are blocking, synchronous, and use urllib.request under the hood. They raise on network errors (catch with try/except) and return [] / None for empty results.
fetch_suggestions(query, filter=Filter.APPS, timeout=10)
Fetch Play Store autocomplete suggestions for a half-query.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query |
str |
required | The half-query to complete |
filter |
Filter |
Filter.APPS |
Filter.APPS, Filter.GAMES, or Filter.ALL |
timeout |
int |
10 |
HTTP timeout in seconds |
Returns: list[str] — the suggestions (already stripped and length-filtered to 3-100 chars). Empty list if the RPC returns nothing or fails to parse.
from google_play_api_unofficial import fetch_suggestions, Filter
# Default: apps-only suggestions
sugs = fetch_suggestions("vpn")
# Include games in the suggestions
sugs = fetch_suggestions("puzzle", filter=Filter.GAMES)
# All types
sugs = fetch_suggestions("tracker", filter=Filter.ALL)
fetch_apps(query, timeout=15)
Fetch the top 30 apps matching a query. The Play Store HTML endpoint caps each query at 30 results; run additional queries with different angles to get more.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query |
str |
required | Search query |
timeout |
int |
15 |
HTTP timeout in seconds |
Returns: list[dict] — each dict has the shape:
{
"package": "org.isoron.uhabits", # str — app id (use this for fetch_app_details)
"title": "Loop Habit Tracker",
"rating": "4.6", # str — the average star rating, or None
"category": "Productivity",
"developer": "Alkaline Software",
"installs": "5,000,000+", # str — the display bucket
"icon": "https://play-lh.googleusercontent.com/...",
"url": "https://play.google.com/store/apps/details?id=org.isoron.uhabits",
}
Empty list if the search page can't be parsed (rare).
from google_play_api_unofficial import fetch_apps
apps = fetch_apps("habit tracker")
for app in apps:
print(app["title"], app["package"], app["installs"])
fetch_app_details(package_id, timeout=15)
Fetch rich details for one app by package id.
| Parameter | Type | Default | Notes |
|---|---|---|---|
package_id |
str |
required | e.g. "com.duolingo" |
timeout |
int |
15 |
HTTP timeout in seconds |
Raises:
AppNotFoundError— package id does not exist on the Play Store (HTTP 404). The exception's.package_idattribute holds the id.urllib.error.HTTPError— for other HTTP errors (e.g. 429 rate limit). Catch if you need to retry.- Other network errors (e.g.
urllib.error.URLError).
Returns: dict with the full details, or None if the page was fetched but the data could not be parsed. See the field reference for the complete shape.
from google_play_api_unofficial import fetch_app_details, AppNotFoundError
try:
d = fetch_app_details("com.duolingo")
except AppNotFoundError:
print("No such app")
else:
print(d["title"], d["score"], d["installs"], d["released"])
Field reference
Output of fetch_app_details (and the enriched results from search --with-details):
| Field | Type | Notes |
|---|---|---|
package |
str or None |
App id (e.g. com.duolingo) |
title |
str or None |
App name |
score |
str or None |
Average star rating (e.g. "4.7") |
ratings_count |
str or None |
Total number of ratings (e.g. "45,891,296") |
reviews_count |
str or None |
Number of written reviews (e.g. "943,469") |
histogram |
list[tuple[int, int]] |
[(stars, count), ...] from 5★ down to 1★ |
installs |
str or None |
Display bucket (e.g. "500,000,000+") |
installs_min |
int or None |
Lower bound of the bucket (e.g. 500_000_000) |
installs_real |
int or None |
Approximate actual install count (e.g. 919_061_546) |
released |
str or None |
First release date (e.g. "May 29, 2013") |
updated |
str or None |
Last update date (e.g. "Jun 2, 2026") |
content_rating |
str or None |
PEGI/ESRB-style age rating (e.g. "Everyone") |
category |
None |
Not in the details payload; populated by search if you join the two |
developer |
str or None |
Developer name |
developer_id |
str or None |
Play Store dev id (e.g. "6957685454452609502") |
developer_email |
str or None |
Support email |
developer_website |
str or None |
Website URL |
developer_address |
str or None |
Physical address |
iap_range |
str or None |
In-app purchase price range (e.g. "$0.99 - $239.99 per item") |
short_description |
str or None |
One-line tagline |
description |
str or None |
Full description, HTML stripped, <br> → newlines |
icon |
str or None |
App icon URL |
screenshots |
list[str] |
URLs of all screenshots (and the feature graphic at index 0) |
url |
str or None |
Play Store URL |
Output of fetch_apps:
| Field | Type |
|---|---|
package |
str |
title |
str |
rating |
str or None |
category |
str or None |
developer |
str or None |
installs |
str or None |
icon |
str or None |
url |
str or None |
Error handling
fetch_suggestionsandfetch_appscan raiseurllib.error.HTTPError(rate limits, etc.) orurllib.error.URLError(network problems). They return[]if the page was fetched but could not be parsed.fetch_app_detailsraisesAppNotFoundErroron 404 and propagates otherHTTPErrors. It returnsNoneonly when the page was fetched but couldn't be parsed.- The CLI catches
AppNotFoundErrorand prints! app not found: <pkg>to stderr, then continues with the next package.
import urllib.error
from google_play_api_unofficial import fetch_apps, fetch_app_details, AppNotFoundError
# fetch_apps returns [] on parse failure but raises on network errors
try:
apps = fetch_apps("habit tracker")
except urllib.error.HTTPError as e:
if e.code == 429:
print("Rate limited — try again in a minute")
else:
raise
# fetch_app_details raises AppNotFoundError for 404
try:
d = fetch_app_details("ch.protonvpn.android")
except AppNotFoundError as e:
print(f"No such app: {e.package_id}")
except urllib.error.HTTPError as e:
if e.code == 429:
print("Rate limited — try again in a minute")
else:
raise
# Multi-app loop with a small sleep to avoid 429s
import time
results = []
for pkg in ["com.duolingo", "com.busuu", "com.babbel"]:
try:
results.append(fetch_app_details(pkg))
except AppNotFoundError:
print(f"skipped (not found): {pkg}")
results.append(None)
except urllib.error.HTTPError as e:
print(f"skipped {pkg}: {e}")
results.append(None)
time.sleep(0.2)
Recipes
Expand a half-query into related terms
google-play-api-unofficial suggest "habit" --json | jq -r '.["habit"][]'
Find established apps in a category
google-play-api-unofficial search "habit tracker" --json \
| jq -r '.["habit tracker"].apps[] | "\(.installs)\t\(.rating)\t\(.package)\t\(.title)"' \
| sort -rn
Sort by installs to find incumbents; by rating to find quality outliers.
In Python:
apps = fetch_apps("habit tracker")
for a in sorted(apps, key=lambda a: a.get("installs") or "", reverse=True):
print(a["installs"], a["rating"], a["package"], a["title"])
Compare two apps side by side
CLI:
google-play-api-unofficial details com.duolingo com.busuu --json \
| jq 'to_entries | map({pkg: .key, released: .value.released, installs: .value.installs, score: .value.score, reviews: .value.reviews_count, iap: .value.iap_range})'
Python:
import json
a = fetch_app_details("com.duolingo")
b = fetch_app_details("com.busuu")
for d in (a, b):
print(f"{d['package']}: {d['title']}")
print(f" released: {d['released']}, updated: {d['updated']}")
print(f" score: {d['score']} ({d['ratings_count']} ratings, {d['reviews_count']} reviews)")
print(f" installs: {d['installs']} (~{d['installs_real']:,} real)")
print(f" iap: {d['iap_range']}")
Collect apps across related queries
Loop 3-5 different seed queries, collect package ids, dedupe, then optionally fetch details for the most promising:
queries = ["habit tracker", "daily routine", "streak app", "todo planner"]
seen = set()
all_apps = []
for q in queries:
for app in fetch_apps(q):
if app["package"] not in seen:
seen.add(app["package"])
all_apps.append(app)
print(f"Found {len(all_apps)} unique apps across {len(queries)} queries")
# Now enrich top N with details
import time
for app in all_apps[:20]:
try:
d = fetch_app_details(app["package"])
app.update({k: v for k, v in d.items() if v not in (None, [], "")})
except Exception as e:
print(f" skipped {app['package']}: {e}")
time.sleep(0.2)
Rank apps by review-to-install ratio
The reviews-to-installs ratio is a strong signal: an app with a small install count but a high ratio is getting unusual engagement.
candidates = []
for app in fetch_apps("fitness coach"):
try:
d = fetch_app_details(app["package"])
except Exception:
continue
if not d or not d.get("installs_real") or not d.get("reviews_count"):
continue
real = d["installs_real"]
# reviews_count is a formatted string like "1,234"
reviews = int(d["reviews_count"].replace(",", ""))
engagement = reviews / real # reviews per install
candidates.append((engagement, d))
time.sleep(0.2)
candidates.sort(reverse=True)
for engagement, d in candidates[:20]:
print(f"{engagement*100:.3f}% {d['package']} {d['title']} reviews={d['reviews_count']} installs={d['installs']}")
CLI equivalent:
google-play-api-unofficial search "fitness coach" --with-details --json \
| jq -r '.["fitness coach"].apps[] | select(.installs_real != null and .reviews_count != null) | "\(.reviews_count)\t\(.installs_real)\t\(.package)"' \
| sort -rn | head -20
Limits and errors
- Search results: 30 per query (Play Store HTML cap; the public endpoint does not paginate via
&start=N. To get more, use multiple seed queries and dedupe bypackage.) - Suggestions: 10 per query (Play Store cap)
- Rate limits: Play Store returns HTTP 429 after sustained scraping. The CLI sleeps 200ms between detail requests. If you hit 429, wait a minute and retry. Multi-second sleeps are safer for bulk jobs.
- HTTP 404 — package id is no longer in the Play Store (deleted, renamed, or never existed). The library raises
AppNotFoundError; the CLI catches it and prints! app not found: <pkg>to stderr. - No results — Play Store returned a page without the expected data chunks. Usually transient; retry.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file google_play_api_unofficial-0.1.0.tar.gz.
File metadata
- Download URL: google_play_api_unofficial-0.1.0.tar.gz
- Upload date:
- Size: 22.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d88a6e8aa5f11fd0d9026f2bfeff2f23121db80928fc7e224e25d0740f2e6c44
|
|
| MD5 |
7f24a50c71f8cf0ce8e3cd2cfb2bd4c0
|
|
| BLAKE2b-256 |
762228a6a26ee1ee325e4128ba8983821c700d8c5609d2dd4ce34ad894cbd8d6
|
File details
Details for the file google_play_api_unofficial-0.1.0-py3-none-any.whl.
File metadata
- Download URL: google_play_api_unofficial-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
293207a0a552e1d53be9d994771f830c91c27557e0d6011585e436595a7c10b0
|
|
| MD5 |
672e5a333276a41c66158cd693da33e9
|
|
| BLAKE2b-256 |
c2580de435a6b36bd6097422170fb30c6adb37b52478ca417c5071435d444550
|