Run pip install pytrends today and pip hands you version 4.9.2, published on 13 April 2023. Follow the project link to its repository and GitHub shows a grey banner across the top: archived by the owner on 17 April 2025, read-only. Behind it sit 136 open issues, frozen where they stood. One of the last, from February 2025, is titled "Stepping out as a maintainer." Another from the same month is titled "Google API Trends changed endpoint." That is the library almost every Google Trends scraper tutorial still tells you to install, usually without mentioning any of it.
The data is not out of reach. But three of the endpoints pytrends calls now return 404, one of its documented configurations raises TypeError on a clean install, and its daily-data helper silently writes a column of NaNs on current pandas. Meanwhile Google shipped two routes that did not exist when most of these guides were written: an official API, in alpha since July 2025, and a public RSS feed that needs no key, no token and no cookie.
Everything below was read from a vendor's own page, from the library's own source, or from the endpoint's own response on 13 August 2026. Where we could not confirm something, the text says so instead of guessing.
What the numbers are, before you collect any
Google's own help page is blunt about what a Trends value means. Each data point "is divided by the total searches of the geography and time range it represents to compare relative popularity," and the result "scales on a range of 0 to 100 based on a topic's proportion to all searches on all topics." Nothing in the series is a search count. A 100 is the busiest bucket in the window you asked for, and nothing more.
Three consequences shape every scraper built on top of it.
Scores are comparable only inside one request. Query five keywords together and the 0-100 scale is shared across all five. Query them one at a time and each gets its own private 100. Stack those separate pulls into one table and you have produced a chart that looks authoritative and means nothing. This one fact quietly ruins more trend datasets than rate limiting ever has.
The data is sampled, and the sample changes. Google states plainly that it does not query the full corpus, because "providing access to the entire data set would be too large to process quickly." Pull the same keyword, same geo, same window on two consecutive days and the numbers move. For a five-year weekly series the wobble is cosmetic. For a low-volume term on a 7-day window it can be most of the signal.
Values arrive as integers. A term that peaks at 4 against its competitors has five possible values for its entire history: 0, 1, 2, 3, 4. No amount of smoothing recovers what the rounding threw away. If your keyword is small relative to whatever else is in the payload, you are not measuring it, you are watching it round.
What the platform exposes, in rough order of how much of it you will actually use:
- Interest over time. The normalized 0-100 series, weekly for windows over 9 months, daily under that, hourly for
now 7-dand shorter. The last row often carriesisPartial = True, meaning the bucket is still filling. Charting it without dropping that row produces a cliff at the right edge of every graph you make. - Interest by region. The same score split by country, sub-region, metro (
DMA, US only) or city. - Related queries. The top and the rising lists. Google's own FAQ defines the label most people misquote: "If you see 'Breakout' instead of a percentage, it means that the search term grew by more than 5000%."
- Related topics. Entities from Google's Knowledge Graph rather than raw strings, which is what makes them language-independent.
- Trending searches. Still published, no longer where the libraries look for it. See below.
What Google broke, and when
Most articles about this topic describe an API surface that has not existed for a while. We requested each endpoint pytrends is hard-coded against and recorded what came back on 13 August 2026:
| Endpoint | Response on 13 Aug 2026 | pytrends method it backs |
|---|---|---|
/trends/api/explore |
400, alive, rejects a request with no req |
build_payload() |
/trends/api/widgetdata/multiline |
400, alive, rejects a request with no token | interest_over_time() |
/trends/api/widgetdata/comparedgeo |
reachable, same token requirement | interest_by_region() |
/trends/api/widgetdata/relatedsearches |
reachable, same token requirement | related_queries(), related_topics() |
/trends/api/autocomplete/<term> |
200, returns JSON, no token needed | suggestions() |
/trends/api/dailytrends |
404 | today_searches() |
/trends/api/realtimetrends |
404 | realtime_trending_searches() |
/trends/hottrends/visualize/internal/data |
404 | trending_searches() |
/trending/rss?geo=US |
200, valid RSS | none, this one is new |
The explore-and-widget path, where the actual analysis lives, still answers. Every trending-searches route the library knows about is gone. The earlier version of this article printed pytrends.trending_searches(pn="united_states") as a working example. It is not one. Google moved that product to the Trending Now page and now serves it as a feed, which almost nobody writing about Trends scraping has noticed.
Three 404s and one new feed.
Method 1: pytrends, and the two places it breaks
pytrends is a "pseudo-API" that speaks to the same internal endpoints the website uses and hands back pandas DataFrames. Its own README describes the deal honestly: "Unofficial API for Google Trends," good "only until Google changes their backend again." The PyPI record still shows 4.9.2 as current, and the repository is read-only, so that sentence has stopped being a joke and become a specification.
pip install pytrends pandasOn a clean virtual environment on 13 August 2026 that resolves to pytrends 4.9.2, pandas 3.0.5 (released 22 July 2026), numpy 2.4.6, requests 2.34.2, urllib3 2.7.0 and lxml 6.1.1. pytrends declares its dependencies as pandas (>=0.25), requests (>=2.0), lxml, with no upper bound anywhere. A library last touched in 2023 is therefore installed on top of a pandas major release from 2026, and pip reports no conflict at all.
Two things go wrong as a direct result.
The proxy configuration in every tutorial raises TypeError
This is the snippet that circulates, and it is the one the previous version of this article printed:
pytrends = TrendReq(
hl="en-US", tz=360,
timeout=(10, 25),
proxies=["https://user:pass@proxy-host:port"],
retries=3, backoff_factor=0.6,
)
pytrends.build_payload(["web scraping"], timeframe="today 12-m", geo="US")
# TypeError: Retry.__init__() got an unexpected keyword argument 'method_whitelist'Inside _get_data, pytrends builds a urllib3 Retry whenever retries or backoff_factor is greater than zero, and it passes method_whitelist=frozenset(['GET', 'POST']). urllib3 deprecated that argument in 1.26 and removed it in 2.0, released on 26 April 2023. pytrends 4.9.2 shipped on 13 April 2023. The last release of the library predates the release that broke it by thirteen days, and nobody was left to publish a one-line fix.
The failure is deterministic and needs no network to reproduce:
from pytrends.request import TrendReq
t = object.__new__(TrendReq) # skip __init__, which makes a live request
t.retries, t.backoff_factor = 3, 0.6 # exactly what the snippet above passes
t.proxies, t.cookies, t.timeout = [], {}, (10, 25)
t.headers, t.requests_args = {"accept-language": "en-US"}, {}
t._get_data("https://trends.google.com/trends/api/explore")
# TypeError: Retry.__init__() got an unexpected keyword argument 'method_whitelist'Two ways out. Pin urllib3<2 in the environment, which drags the rest of your stack backwards and is worth doing only in a container dedicated to this job. Or leave retries and backoff_factor at their defaults of zero, never touch that code path, and write your own retry loop. The second is better anyway, because the loop you write can distinguish a 429 from a 502, and urllib3's cannot tell you which one you got.
The daily-data helper silently produces NaNs
pytrends ships pytrends.dailydata.get_daily_data, which does the thing everyone eventually needs: it pulls one month of daily data at a time, pulls the whole span at monthly resolution once, and multiplies the daily values by the monthly level so that separate requests land on a shared scale. Good design. The implementation contains this line:
complete[f'{word}_monthly'].ffill(inplace=True) # fill NaN valuespandas 3.0 made Copy-on-Write the default. Under CoW that call operates on a temporary copy of the column and throws the result away. pandas emits a ChainedAssignmentError warning rather than an exception, and the warning says exactly what happened: "Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy."
The NaNs stay. The next line divides that column by 100 to build scale, so scale is NaN for every day that is not a month boundary. The line after that multiplies the unscaled daily data by scale, so the output column your whole analysis reads is NaN almost everywhere. Nothing raises, nothing exits non-zero, and a scheduled job writes a table of nulls until somebody opens the chart.
The fix is one line, applied to your local copy:
complete[f"{word}_monthly"] = complete[f"{word}_monthly"].ffill()A third detail worth knowing. TrendReq(...) is not inert configuration. Its constructor calls GetGoogleCookie(), which issues a live requests.get to https://trends.google.com/trends/explore/?geo=US for an NID cookie. That call uses the bare requests module, outside the session carrying your retry settings. If it fails behind a proxy, pytrends prints Proxy error. Changing IP, pops an entry off your proxy list, and re-raises once the list empties. The object you thought you were constructing is a network request, and it is the first thing that fails in CI.
A first pull that works
import pandas as pd
from pytrends.request import TrendReq
# retries and backoff_factor stay at 0 on purpose: see above.
pytrends = TrendReq(hl="en-US", tz=360, timeout=(10, 25))
kw_list = ["web scraping", "data extraction", "screen scraping"]
pytrends.build_payload(kw_list, timeframe="today 12-m", geo="US")
iot = pytrends.interest_over_time()
iot = iot[~iot["isPartial"]].drop(columns="isPartial") # drop the unfinished bucket
iot.to_csv("trends_interest_over_time.csv")build_payload sets the scope: the keyword list, timeframe ("today 5-y", "today 12-m", "now 7-d", or an explicit "2024-01-01 2025-12-31"), geo ("" worldwide, "US", "GB", "US-CA"), an optional numeric cat, and gprop, restricted in the source to '', 'images', 'news', 'youtube' or 'froogle'. pytrends does not enforce the five-keyword limit. It will build a payload with twelve keywords and let Google reject it, which matters when you write the batching code.
related = pytrends.related_queries()
rising = related["web scraping"]["rising"] # "Breakout" means growth over 5000%
region = pytrends.interest_by_region(resolution="COUNTRY", inc_low_vol=True)Swap resolution for "REGION", "DMA" or "CITY" to drill down. inc_low_vol=True includes places under Google's reporting threshold, which mostly means it includes more zeros.
Method 2: calling the endpoints yourself
When the wrapper is unmaintained, understanding what it wraps stops being an academic exercise. The flow is a two-step handshake.
First, a POST to https://trends.google.com/trends/api/explore carrying hl, tz and a URL-encoded JSON req that describes your comparison items, timeframe and geo. The response is a list of widgets, each with its own token. Second, a GET against the widget's own endpoint (/widgetdata/multiline for the timeline, /comparedgeo for the map, /relatedsearches for the query lists), passing that token and a matching req.
Responses carry an anti-JSON-hijacking prefix that you strip before parsing. The prefix is not the same length everywhere, which is why pytrends trims 4 characters from the explore response and 5 from widget responses. Guessing one constant for both is a classic source of JSONDecodeError at 3 a.m.
The autocomplete endpoint needs no token at all, which makes it the cleanest way to see the shape of the thing. This request ran on 13 August 2026:
import json, requests
r = requests.get(
"https://trends.google.com/trends/api/autocomplete/pizza",
params={"hl": "en-US", "tz": 360},
headers={"accept-language": "en-US"},
)
payload = json.loads(r.text[5:]) # strip the )]}', prefix
for t in payload["default"]["topics"]:
print(t["mid"], "|", t["title"], "|", t["type"])The response begins )]}', and then:
{"default":{"topics":[
{"mid":"/m/0663v","title":"Pizza","type":"Dish"},
{"mid":"/m/0dfxdnc","title":"Pizza dough","type":"Food"},
{"mid":"/g/11fl7dydwb","title":"Pizza Oven","type":"Topic"},
{"mid":"/g/11k19hmrkk","title":"Licorice Pizza","type":"2021 film"},
{"mid":"/m/05m1lt","title":"Pizza delivery","type":"Topic"}
]}}Those mid values are the most underused feature in Google Trends. A mid goes into the payload wherever a keyword string would go. Query the string pizza and you measure the English word, competing with "Licorice Pizza" and every other collision. Query /m/0663v and you measure the dish as an entity, across languages and spellings, which is what you actually wanted when you compared demand in Germany against demand in Brazil. Autocomplete is how you find the right mid, and it costs one unauthenticated GET.
Rolling your own is reliable, and you own cookie handling, token refresh and proxy rotation when the payload format shifts. Which it does, as the archived issue tracker documents at length.
Method 3: the official Google Trends API, still in alpha
Google announced an official interface on the Search Central blog in July 2025, under the title "Introducing the Google Trends API (alpha)". It is the first supported route Google has offered for this data, and it remains gated.
The most detailed public account of using it is a test writeup by Tobias Willmann, published 6 November 2025. It reports the service at https://searchtrends.googleapis.com/v1alpha/, authenticated with OAuth 2.0 through a desktop client credential rather than a plain API key, with time resolutions of DAY, MONTH and YEAR, a lookback of about five years, and a quota near 10,000 data points per day. It returns two fields where the public interface returns one: searchInterest, and a scaledSearchInterest normalized to 0-100. The same writeup warns that the scaled figure still depends on the requested timeframe and resolution, so the comparability trap survives the move to an official API. No hourly resolution, no real-time data.
Two honest limits on what we can tell you. The discovery document at searchtrends.googleapis.com returns 403 unauthenticated for both v1 and v1alpha, so whether the service has graduated past the alpha path is not knowable from outside. And Google's Search Central feed on 13 August 2026 carries ten posts from March to July 2026, none about Trends, so no general-availability announcement has landed on that channel. If your project can wait, this is the route to watch.
Method 4: the RSS feed almost nobody mentions
While the dailytrends and realtimetrends endpoints were being retired, Google started serving trending searches as a plain feed. It is live, it is documented by nothing much, and it needs no key:
https://trends.google.com/trending/rss?geo=USFetched on 13 August 2026, it returned a valid RSS channel titled "Daily Search Trends" with ten items, dated 12 August 2026. Each item carries the search term as its title, plus Google's own extension elements: ht:approx_traffic with a rounded volume figure, ht:picture and ht:picture_source, and one or more ht:news_item blocks, each holding ht:news_item_title, ht:news_item_snippet, ht:news_item_url, ht:news_item_picture and ht:news_item_source.
That ht:approx_traffic field deserves attention. It is the only place in the entire Trends product where Google publishes something resembling an absolute number rather than a 0-100 index. It is rounded hard and only exists for terms that trend. But it exists.
import urllib.request
import xml.etree.ElementTree as ET
url = "https://trends.google.com/trending/rss?geo=US"
with urllib.request.urlopen(url) as resp:
root = ET.fromstring(resp.read())
for item in root.iterfind("./channel/item"):
term = item.findtext("title")
traffic = item.findtext("{*}approx_traffic") or "-"
stories = list(item.iterfind("{*}news_item"))
print(f"{term:<38} {traffic:>10} {len(stories)} stories")The {*} wildcard, supported by ElementTree since Python 3.8, matches the ht: namespace without you having to hard-code its URI. Swap geo=US for any country code. Poll it every fifteen minutes and you have a trending-topics pipeline with no proxies, no token handshake and no rate-limit strategy, because you are consuming something Google publishes for consumption.
Nothing else in this article is that cheap.
Method 5: commercial APIs, with the prices
If babysitting endpoints is not the job you were hired to do, several providers sell a hosted Trends endpoint. You send a keyword, timeframe and geo; they handle rendering, proxies and blocking, and return structured JSON. Prices below were read from each vendor's own page on 13 August 2026.
| Provider | What it sells | Price as published |
|---|---|---|
| SerpApi | Trends widgets as data_type: TIMESERIES, GEO_MAP, GEO_MAP_0, RELATED_TOPICS, RELATED_QUERIES, plus a separate Trending Now endpoint |
Free 250 searches/month; $25 for 1,000; $75 for 5,000; $150 for 15,000; $275 for 30,000 |
| Apify Google Trends Scraper | Interest over time, by region and city, related queries and topics; JSON, CSV, Excel, XML output | $0.30 per 1,000 results |
| DataForSEO | google_trends/explore/live, up to 5 keywords, item_types for graph, map, topics list and queries list |
Not printed on the endpoint documentation; the pricing page did not answer an automated read |
| Bright Data SERP API | General SERP extraction, quoted here as the market rate for this class of work | $1.50 per 1,000 requests pay-as-you-go; $499/month for 380,000, then $1.30 per 1,000; free tier of 5,000 records/month |
SerpApi's own documentation states the constraint that catches people out: "The maximum number of queries per search is 5," and that limit applies only to TIMESERIES and GEO_MAP. The other data types accept one query per search. So a job that pulls related queries for 200 brands is 200 requests, not 40.
Work the arithmetic before you pick a tier. Tracking 500 keywords weekly, batched four at a time against a shared anchor, is 125 requests a week and 6,500 a year. On SerpApi's $25 tier that is about $163 a year in search credits. The Apify actor bills per row rather than per call, so estimate that one in results. Bright Data does not name Google Trends among its SERP targets, so confirm coverage before you build against it.
Paid access is cheap at the volumes most teams actually run.
Method 6: CSV export from the interface
For a one-off, skip the code. Open trends.google.com, enter your terms, set the window and region, and use the download control on any panel to export that widget. You get what is on screen, correctly normalized, with no rate limit to think about.
The reason it does not scale is worth naming precisely. Every export is one keyword set on one scale. Twenty exports give you twenty incompatible 0-100 series, and stitching them together by hand reintroduces the exact error a scraper exists to avoid. Right for one comparison, wrong for the twenty-first.
Rate limits, and what a 429 actually means
The most common failure when you scrape Google Trends is HTTP 429 Too Many Requests. pytrends treats 429 as retryable alongside 500, 502 and 504, and raises TooManyRequestsError, a subclass of ResponseError that carries the original response object on .response. Read that object. A 429 after two calls means something about your IP or fingerprint, while a 429 after two hundred means you simply went too fast, and the two have opposite fixes.
Back off exponentially, in your own code. Since the library's built-in retry path is the one that raises TypeError, this loop is not optional:
import random, time
from pytrends.request import TrendReq
from pytrends.exceptions import TooManyRequestsError, ResponseError
def safe_interest_over_time(pytrends, kw_list, attempts=5, **kwargs):
for attempt in range(attempts):
try:
pytrends.build_payload(kw_list, **kwargs)
return pytrends.interest_over_time()
except TooManyRequestsError:
wait = 5 * (2 ** attempt) + random.uniform(0, 3) # jitter matters
print(f"429 on {kw_list}, sleeping {wait:.1f}s")
time.sleep(wait)
except ResponseError as e:
code = getattr(e.response, "status_code", "?")
print(f"HTTP {code} on {kw_list}, not retrying")
raise
raise RuntimeError(f"still rate-limited after {attempts} attempts: {kw_list}")The jitter is not decoration. Without it, a pool of workers that all hit a 429 in the same second will retry in the same second, forever, in lockstep.
Rotate IPs. Trends limits are enforced per address, so rotating proxies, and residential proxies for the harder cases, are the real fix at volume. pytrends accepts a list and rotates through it, though remember that the constructor fetches its cookie through the first entry before you have run a single query.
Throttle on purpose. The library's own get_daily_data sleeps 5 seconds between month-sized pulls, with the comment "don't go too fast or Google will send 429s." That is the maintainers' own calibration and a reasonable floor. A few seconds of time.sleep beats an hour in the penalty box, and the arithmetic is small: one extra second per request across 6,500 requests a year is under two hours total.
Batch around an anchor. Five keywords per request is the ceiling, so to rank fifty terms you need eleven batches with one keyword in common, then a rescale onto the anchor's level:
def rescale(batch, anchor, anchor_reference_mean):
"""Put one batch onto a shared scale using the anchor's level."""
observed = batch[anchor].mean()
if observed < 5: # anchor too small: the batch is noise
raise ValueError(f"anchor '{anchor}' averaged {observed:.1f}, pick a busier one")
factor = anchor_reference_mean / observed
return batch.drop(columns=[anchor]) * factorThe guard clause is the important part. An anchor smaller than the terms you are measuring rounds toward zero inside most batches, the factor explodes, and you publish a keyword that appears to have grown four hundred percent because a divisor was 1 instead of 1.4. Pick an anchor busier than everything you compare against it, and keep the same one for the whole project.
What breaks at ten thousand keywords
A single comparison is forgiving. A nightly job across a large keyword set fails in ways a one-off never shows you.
Scale drift between runs. Yesterday's batch and today's were each normalized to their own peak. If one term spikes today, every other term in that payload gets pushed down, and a naive series reads that as five keywords declining. Store the raw batch output next to the rescaled values so you can tell a real fall from an arithmetic one.
Sampling noise looks like a trend. Because Google samples, the same request on two days returns different numbers. On a weekly five-year series that is invisible. On a now 7-d pull of a small term it is most of the variance. If a decision rests on a small term, pull it three times across a day and take the median.
The partial bucket. isPartial marks the final row as incomplete. Left in place, it makes every keyword appear to collapse at the right edge of the chart, and it makes every "is this term declining" alert fire simultaneously each morning.
Silent nulls. The Copy-on-Write bug above is one case of a general rule: assert on your output. A row count proves nothing, because the rows were all there. Check the null fraction of the column you rescaled, and fail the run when it moves.
Cost arrives in requests, not keywords. Related queries and related topics take one keyword per call at every provider we checked. A dashboard covering 200 terms with interest, region and rising queries is 40 timeline calls plus 200 plus 200. That is 440, not 40, and the pricing table above is denominated in calls.
Geographic granularity has holes. DMA resolution is US-only. City resolution returns empty frames for terms below Google's reporting threshold there, which for most keywords is most cities. Build for a sparse result, not a missing one.
If keeping that pipeline honest stops being the interesting part of your week, this is the point where a scheduled feed from a managed extraction service costs less than the engineer-hours it replaces.
Where Google Trends stops being the right tool
There is no absolute volume in it. Trends tells you shape, not size. A keyword at 100 might get four hundred searches a month or four million, and the index is identical either way. Anyone who tells you Trends gives search volume is selling a modeled estimate on top of it. Pair the shape with search volume data from a source that reports counts, and use each for what it measures.
Zero does not mean zero. It means "below the threshold for this comparison," which depends entirely on what else is in the payload. Put a giant next to four small terms and all four flatline at 0 while remaining perfectly healthy businesses.
Correlation on search data has a famous failure mode. Google Flu Trends predicted influenza-like illness from search behavior, ran for years, and drifted badly wrong. The autopsy is "The Parable of Google Flu: Traps in Big Data Analysis" by Lazer, Kennedy, King and Vespignani in Science, March 2014, which traced the failure to a model fitted on correlations that Google's own product changes then invalidated. Google retired the public site in 2015. The lesson is not that search data is useless. It is that a model built on which queries correlate with your target will quietly rot every time the search interface changes, and the search interface changes constantly.
It is a demand signal, not a forecast. Rising queries tell you what people are typing now. Whether that becomes a purchase, a subscription or a vote is a question Trends cannot answer.
Finding correlated search terms after Google Correlate
Google once ran a companion tool, Google Correlate, that inverted Trends: you supplied a time series, such as your weekly organic traffic, and it returned the queries whose popularity tracked your curve most closely, ranked by Pearson correlation. Google retired it in December 2019. As of 13 August 2026 the URL trends.google.com/correlate/ returns 404, so every tutorial still pointing there sends readers into a wall.
The technique reproduces in about fifteen lines. Pull the Trends series for a set of candidate terms, join each against your own series, correlate:
import time
import pandas as pd
from pytrends.request import TrendReq
pytrends = TrendReq(hl="en-US", tz=360, timeout=(10, 25))
# `traffic`: your own DataFrame, DatetimeIndex, one "visits" column,
# resampled to the same weekly grid Trends returns.
candidates = ["data mining", "machine learning", "analytics",
"python tutorial", "e-commerce"]
scores = {}
for term in candidates:
df = safe_interest_over_time(
pytrends, [term], timeframe="2023-01-01 2025-12-31", geo="US")
series = df[~df["isPartial"]][term]
merged = traffic.join(series, how="inner")
if len(merged) < 30: # too few overlapping weeks to trust
continue
scores[term] = merged["visits"].corr(merged[term])
time.sleep(3)
ranked = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)Three things the naive version of this loop gets wrong, including the one printed in the earlier version of this article. It queried each term in its own payload, which is correct here and only here: you are correlating each term against your own series independently, so the private 0-100 scales do not interfere. It did not filter isPartial, so the last week of every series was an artifact. And it reported the top of ranked without saying how many candidates went in.
That last point is the one that matters statistically. Test 100 candidate terms at the conventional 5% threshold and roughly five will look significant by construction, with no relationship to anything. Screening a thousand terms guarantees a handful of beautiful, meaningless matches. Treat a high coefficient as a prompt to go look at Search Console and your own analytics, never as a finding. Combine it with keyword research scraping so you are correlating against terms that make commercial sense rather than whatever the loop happened to be fed.
If you would rather buy the answer: Exploding Topics publishes plans at $39, $99 and $249 a month, with CSV export starting at the $99 tier. Glimpse markets absolute search volume and year-over-year growth on top of Trends but publishes no prices on its pricing page, so you get a number by asking. Neither is Correlate. Both save you the loop.
A worked example: which products lead a niche
Say you run electronics retail and want to know which devices to feature. The workflow is three requests and one rule.
- Compare the broad players in one payload so the scores share a scale, and read who leads over the last twelve months.
- Pull the rising related queries for the leader to find specific models gaining momentum. One call per brand, because related queries take one keyword.
- Re-compare at the product level by putting the leading models head to head in a single payload.
brands = ["samsung", "apple", "google pixel", "xiaomi", "oneplus"]
brand_interest = safe_interest_over_time(
pytrends, brands, timeframe="today 12-m", geo="")
brand_interest = brand_interest[~brand_interest["isPartial"]].drop(columns="isPartial")
print(brand_interest.mean().sort_values(ascending=False))
leader = brand_interest.mean().idxmax()
pytrends.build_payload([leader], timeframe="today 12-m", geo="")
print(pytrends.related_queries()[leader]["rising"].head(10)) # hot modelsThe rule: never carry a score from step 1 into step 3. Those are two different payloads and two different scales, and the only number that travels between them is the ordering.
The output is a defensible input to a merchandising or ad decision, produced in minutes rather than a week of chart-reading. Where it needs to arrive cleaned and on a schedule instead of on a laptop, that is a data-as-a-service problem rather than a scripting one.
Is scraping Google Trends allowed
The data is public and free, and pulling it for research or analytics is ordinary practice. The interesting part is what Google's own files say, because it is not what most articles assume.
We read both relevant robots files on 13 August 2026. On www.google.com, the User-agent: * block contains Disallow: /trends/api, along with Disallow: /trends/explore? and several other Trends paths. On trends.google.com, which is the host the widget endpoints actually live on, the entire file is one user-agent block with two rules: Disallow: /explore? and Disallow: /trends/explore?. The /trends/api/ prefix is not named there.
Read that carefully before you build a policy on it. The human-facing explore page is disallowed on both hosts. The API paths are disallowed on one host and unmentioned on the other, and whether that omission is deliberate is not knowable from outside Google. What is unambiguous: the internal endpoints are not a supported public API, Google's terms address automated access separately from robots.txt, and heavy collection gets an address throttled whatever any file says. For the broader picture on where automated collection sits legally, see is web scraping legal.
The two routes with no ambiguity at all are the RSS feed and the alpha API. Both are published by Google for programmatic consumption. If your use case fits either, take it.
Scrape politely, cache aggressively, and never re-request a window you already hold. Trends history does not change retroactively, so a cached 2023 series is as good today as the day you pulled it.
Bottom line
For one comparison, the download control in the interface is the correct tool and everything else is overhead. For a real Google Trends scraper, the picture in August 2026 is specific rather than general.
pytrends still works for interest over time, interest by region, related queries and related topics, provided you leave its retry parameters alone and patch the ffill line before you trust get_daily_data. Its trending-searches methods are dead, and the repository that would fix them is read-only. trendspy is the most credible successor, with trending_now_by_rss built on the same feed described above, but calibrate your expectations: 115 stars and a last release of 0.1.6 on 25 December 2024 describe one person's side project, not an institution.
The endpoints themselves stay the most durable option if you own the maintenance, and topic mid values make them do things the wrappers rarely expose. The RSS feed is free, supported and ignored by nearly everyone writing about this. The alpha API is the right long-term bet and the wrong short-term dependency. Hosted APIs start at $25 per thousand calls and remove the category.
And when the requirement is trend and demand data delivered on a schedule, cleaned, comparable across batches and ready to import, that is a pipeline rather than a script, which is what our web scraping service builds and keeps running.