DEV Community

Rakshit Solanki
Rakshit Solanki

Posted on

Pull Google Trends data into Python with one API call (no pytrends, no cookies)

Disclosure: I built and sell the Apify actor used in this post (Google Trends Scraper). It is pay-per-use ($2 per 1,000 search terms that return data; failed or empty terms are not charged). Everything below was run against the real actor on 2026-10-30; the numbers shown are from that run.

If you have ever scripted Google Trends, you know the pain: there is no official API, pytrends breaks whenever Google changes something, and rate limits (HTTP 429) hit quickly from datacenter IPs. In this post I will show a small, dependency-light Python workflow that handles the retries and proxy rotation on the server side, so your code is just "POST JSON, get JSON".

You will end up with a reusable fetch_trends() function, a comparison of several keywords in one Canada-only query, a list of rising related queries, and a CSV you can open in a spreadsheet.

Setup

  1. Create a free Apify account and copy your API token from Settings → Integrations.
  2. pip install requests
  3. Export the token: export APIFY_TOKEN=apify_api_xxx (never hard-code it).

Step 1: a reusable fetch function

Apify's run-sync-get-dataset-items endpoint starts the actor, waits for it to finish, and returns the results in the HTTP response. It is ideal for short jobs (it times out after 5 minutes, so for large batches use the async start-poll-read pattern shown in the second tutorial of this series, or the Apify client library).

import os
import requests

TOKEN = os.environ["APIFY_TOKEN"]
ACTOR = "flyingnimbus~google-trends-scraper"
URL = f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items"


def fetch_trends(terms, geo="", time_range="past_12_months", compare=False):
    """Run the actor synchronously and return its dataset items as a list of dicts."""
    payload = {
        "searchTerms": terms,
        "compareTerms": compare,
        "geo": geo,                      # "" = worldwide, "CA", "US-CA", ...
        "timeRange": time_range,
        "interestOverTime": True,
        "interestByRegion": True,
        "relatedQueries": True,
        "proxyConfiguration": {"useApifyProxy": True},
    }
    resp = requests.post(
        URL,
        headers={"Authorization": f"Bearer {TOKEN}"},
        json=payload,
        timeout=300,
    )
    resp.raise_for_status()
    return resp.json()


if __name__ == "__main__":
    items = fetch_trends(["python", "ruby"], geo="CA", compare=True)
    print(len(items), "item(s)")
    item = items[0]
    print(item["status"], item["geo"], item["timeRange"])
    print(item["averageInterest"])
Enter fullscreen mode Exit fullscreen mode

Key input fields: searchTerms (list), geo ("" for worldwide, or CA, US-CA, and so on), timeRange (past_7_days, past_12_months, past_5_years, all, ...), compareTerms (put up to 5 terms on one shared 0-100 scale, like the Trends "compare" view) and searchProperty (web, youtube, news, images, shopping).

My run of the __main__ block printed:

1 item(s)
ok CA today 12-m
{'python': 58.13, 'ruby': 25.83}
Enter fullscreen mode Exit fullscreen mode

With compareTerms=True you get one item per comparison group, not one per term. With compareTerms=False you get one item per term.

Step 2: weekly series, rising queries, CSV

Each result item contains interestOverTime (a list of {time, values} points), interestByRegion, relatedQueries (keyed by term, each row tagged top or rising) and averageInterest. Here is a small report script:

import csv
from trends_fetch import fetch_trends


def weekly_series(item):
    """Yield (period_start, {term: value}) from one result item."""
    for point in item.get("interestOverTime", []):
        yield point["time"][:10], point["values"]


def rising_queries(item, limit=5):
    """Return {term: [rising related queries]} for a result item."""
    out = {}
    for term, rows in (item.get("relatedQueries") or {}).items():
        rising = [r for r in rows if r["list"] == "rising"]
        out[term] = [(r["query"], r["value"]) for r in rising[:limit]]
    return out


def to_csv(item, path):
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        terms = list(item["averageInterest"].keys())
        writer.writerow(["period_start", *terms])
        for day, values in weekly_series(item):
            writer.writerow([day, *[values.get(t, "") for t in terms]])


if __name__ == "__main__":
    items = fetch_trends(["python", "ruby", "php"], geo="CA", compare=True)
    for item in items:
        if item["status"] != "ok":
            print("skipped:", item["searchTerms"], item["status"])
            continue
        print("average interest:", item["averageInterest"])
        print("rising:", rising_queries(item))
        to_csv(item, "trends_ca.csv")
    print("wrote trends_ca.csv")
Enter fullscreen mode Exit fullscreen mode

Output from my run (Canada, last 12 months, three terms compared):

average interest: {'python': 57.4, 'ruby': 25.81, 'php': 20.98}
wrote trends_ca.csv   # 53 weekly rows
Enter fullscreen mode Exit fullscreen mode

The CSV starts period_start,python,ruby,php followed by rows like 2025-10-26,63,23,21.

Gotchas I hit (so you do not have to)

  • Ambiguous terms. The "rising" list for ruby was dominated by celebrity names, and python surfaced restaurant queries. Google Trends measures search terms, not topics. Use the category field or more specific phrases like ruby on rails.
  • Relative, not absolute. Values are scaled 0-100 against the peak in your query. They are not search volumes. Comparing across separate queries is invalid; use compareTerms to share a scale.
  • Check status. Each item has a status field. Skip anything that is not "ok", as the script above does, rather than assuming data is present.
  • Partial last point. The latest period can be flagged isPartial: true; drop it before computing growth rates.

Compare mode versus separate queries

Should you pass compareTerms=True or query each term alone? Use compare mode when you care about relative popularity: the values for every term share one scale, so "python 57 vs ruby 26" is a meaningful statement. Use separate queries when you care about each term's own shape over time, for example to spot seasonality of a niche keyword that would look flat next to a giant like python. The cost is per comparison group in compare mode and per term otherwise, so comparing five terms at once is also the cheapest way to get a shared scale.

Ideas to build on this

  • Run it weekly on a keyword list and chart your niche's seasonality before you plan content.
  • Feed rising queries into a content calendar or a keyword-research spreadsheet.
  • Use interestByRegion to pick which provinces or states to target for local ads.
  • Schedule it in the Apify Console (Schedules) and push results to a webhook or Google Sheet.

What it costs

At $2 per 1,000 terms, the three-term comparison above is one comparison group, so roughly a fifth of a cent. A 500-keyword weekly refresh is about $1 per run. Apify's free plan includes some monthly platform credit; check their current pricing page for the exact amount.

Wrap-up

The trick is moving the fragile parts (sessions, proxies, backoff) out of your codebase and behind a stable JSON interface. If you hit an edge case or want another field exposed, open an issue on the actor page.

Top comments (0)