DEV Community

carlos-calatayud
carlos-calatayud

Posted on

Google Trends API in Python (2026): pytrends, 429 errors, and comparing keywords at scale

Google Trends is still one of the few free signals for "what are people searching for, relatively". But if you try to use it from code, you quickly run into three problems:

  1. There is no generally available official API.
  2. The unofficial routes (pytrends and friends) break or return 429 Too Many Requests.
  3. Even when you get data, numbers from different requests are not comparable, because each request is rescaled to its own 0–100.

This post is a practical guide to all three, with Python you can copy. It is a different angle from my earlier post on comparing 100 keywords on one scale: here the focus is getting data reliably into Python and pandas, and knowing which option to pick.

Option 1: the official Google Trends API (alpha)

Google announced an official Trends API in July 2025 as an alpha with limited access by application. If you get in, use it — it's the cleanest route (consistently scaled data across requests). As of writing, access still isn't open to everyone, so most developers need one of the options below. Check Google's current status before you build on it; I'm not going to guess at its quotas or terms.

Option 2: pytrends

pytrends wraps the same internal endpoints the Trends website uses. The basic call:

from pytrends.request import TrendReq

pt = TrendReq(hl="en-US", tz=360)
pt.build_payload(["python", "javascript", "rust"], timeframe="today 12-m", geo="US")
df = pt.interest_over_time()          # index = date, one column per keyword, + isPartial
print(df.tail())
Enter fullscreen mode Exit fullscreen mode

Things that bite people:

  • Max 5 keywords per payload. (The website now accepts more terms in Explore, but pytrends payloads are still built around 5.)
  • The repo is archived and no longer maintained, so when Google changes something, fixes come from forks or not at all.
  • 429 errors. Google rate-limits per IP and session. A loop of a few dozen requests from a laptop is often enough to get blocked for a while, and from cloud/datacenter IPs it can happen almost immediately.
  • tz and hl matter. Weekly buckets shift with timezone; mismatched settings are a common reason why "pytrends doesn't match the UI".

Surviving 429s

There's no magic header. What works is pacing, backoff, resuming and — for volume — more IPs:

import json, os, random, time
from pytrends.request import TrendReq
from pytrends.exceptions import TooManyRequestsError

CACHE = "trends_cache.json"
cache = json.load(open(CACHE)) if os.path.exists(CACHE) else {}
pt = TrendReq(hl="en-US", tz=360, retries=0)

def fetch(batch, timeframe="today 12-m", geo="US", tries=6):
    key = "|".join(batch) + f"|{timeframe}|{geo}"
    if key in cache:
        return cache[key]
    for i in range(tries):
        try:
            pt.build_payload(batch, timeframe=timeframe, geo=geo)
            df = pt.interest_over_time().drop(columns="isPartial", errors="ignore")
            cache[key] = df.reset_index().astype({"date": str}).to_dict("list")
            json.dump(cache, open(CACHE, "w"))
            time.sleep(random.uniform(8, 20))          # be polite between calls
            return cache[key]
        except TooManyRequestsError:
            wait = min(900, 30 * 2 ** i) + random.uniform(0, 10)
            print(f"429 on {batch}, sleeping {wait:.0f}s")
            time.sleep(wait)
    raise RuntimeError(f"Gave up on {batch}")
Enter fullscreen mode Exit fullscreen mode

The cache is the important part: when (not if) you get blocked, you restart and don't lose the work already done.

Option 3: paid SERP-style APIs and hosted scrapers

Services such as SerpApi, DataForSEO and many Apify Actors expose Google Trends behind a normal REST API with proxies and retries handled for you. You trade money for not maintaining scraping code. When comparing them, look at:

  • Price per what? Per request, per keyword, per result row — these differ a lot for the same job.
  • How many keywords per request and whether the output is already merged.
  • Whether multi-batch results are put on one scale (most aren't — see next section).

The real trap: numbers from different requests don't compare

Say you fetch ["python", "javascript", "rust", "go", "kotlin"] and then ["swift", "ruby", "php", "scala", "elixir"]. Both batches come back with something at 100. That doesn't mean the top term in each batch is equally popular — each batch is divided by its own maximum.

Trends values are: share of searches for the term in that place and time, divided by the maximum share among the terms in the request, times 100, rounded to an integer. So concatenating batches gives you numbers on different, unknown scales.

Fix: an anchor term in every batch

Put the same anchor term in every request, then rescale each batch so the anchor lines up:

import pandas as pd

def compare(keywords, anchor, **kw):
    others = [k for k in keywords if k != anchor]
    frames, ref_sum = [], None
    for i in range(0, len(others), 4):                 # anchor + 4 = 5 terms
        batch = [anchor] + others[i:i + 4]
        df = pd.DataFrame(fetch(batch, **kw)).set_index("date").astype(float)
        s = df[anchor].sum()
        if s == 0:
            raise ValueError("Anchor is zero in this batch; pick a bigger anchor")
        ref_sum = ref_sum or s
        df = df * (ref_sum / s)                        # align to batch 1
        frames.append(df if not frames else df.drop(columns=anchor))
    out = pd.concat(frames, axis=1)
    return out * 100 / out.max().max()                 # back to 0-100 overall

data = compare(["python", "javascript", "rust", "go", "kotlin",
                "swift", "ruby", "php", "scala", "elixir"], anchor="javascript")
print(data.mean().sort_values(ascending=False).round(1))
Enter fullscreen mode Exit fullscreen mode

Why the sum of the anchor instead of week-by-week ratios? Because every value is a rounded integer. A week where the anchor is 2 vs 3 is a 50% "difference" from rounding alone. Summing over the whole period smooths that out.

Choosing the anchor

  • It should be in the middle of your keyword set. If it's huge, small terms round to 0 next to it. If it's tiny, big terms flatten its signal to 0–1.
  • If your list spans several orders of magnitude, chain through a bridge term: batch A = big anchor + medium term; batch B = medium term + small terms. Multiply factors along the chain. Each hop adds some error, so keep chains short.
  • Anything whose best raw value is under ~5 in its batch is low precision. Flag it rather than treating it as a solid number.

Other sources of noise

  • Sampling. Google computes Trends on a sample, so the same request on different days can differ by a few points. Fetch all batches in one session, and for important decisions average a couple of runs.
  • Identical parameters. geo, timeframe, category and property (web / YouTube / News / Shopping / Images) must be the same in every batch, or the anchor trick is meaningless.
  • Partial last period. Drop rows with isPartial == True before computing averages.

For academic-grade calibration across thousands of queries, look at G-TAB (Google Trends Anchor Bank) from EPFL, which builds a whole ladder of anchors.

Exporting to CSV / Google Sheets

Once everything is on one scale, a wide CSV is the most convenient format for spreadsheets:

data.drop(columns="isPartial", errors="ignore").to_csv("trends_wide.csv")
summary = pd.DataFrame({
    "average": data.mean(), "peak": data.max(), "peak_date": data.idxmax(),
    "latest": data.iloc[-1],
}).sort_values("average", ascending=False)
summary.to_csv("trends_summary.csv")
Enter fullscreen mode Exit fullscreen mode

Import trends_wide.csv into Google Sheets (File → Import) and you can chart all series together, which you can't do with batches of 5 on separate scales.

Which option should you use?

Situation Reasonable choice
A few comparisons, occasionally Google Trends website or pytrends
You have official API access Official API
Dozens of keywords, one-off analysis pytrends + anchor code above + patience
Recurring jobs, many keywords, no time to babysit 429s A hosted API / scraper

Honest limits (of every option)

  • Trends is relative, not search volume. "Twice as high" means twice the share of searches, not a number of searches.
  • Very small terms are often reported as 0 relative to bigger ones. No method recovers information Google doesn't return.
  • Results vary slightly between fetches because of sampling.
  • Unofficial methods can break whenever Google changes its internal endpoints.

If you'd rather not run this yourself

Disclosure: I built a hosted version of exactly this workflow as an Apify Actor — Google Trends Compare – Bulk 100 Keywords API. You send up to 100 keywords; it handles batching, anchor/bridge chaining, retries and proxies, and returns one 0–100 scale as CSV/JSON plus a summary (average, peak, latest) and an optional self-check against a direct Google query. It's pay-per-use: about $2 per 1,000 keywords compared. The free MIT Python code does the same chaining if you prefer to run it yourself.

Questions or a case where the anchor method gives odd results? Leave a comment — those edge cases are the interesting part.

Top comments (0)