Google Search API in Python
Get Google search results in Python with the serpkite package: first query, country and city, paging, asyncio, CSV export, Markdown for LLMs and error handling.
This guide gets Google results into a Python program, from the first query to concurrent searches with error handling. Every snippet runs as is with Python 3.9+ and the official serpkite package. A runnable copy of the examples lives in SerpKite/google-search-api on GitHub.
Each search costs 1 credit, from $0.60 per 1,000 on the largest pack. Failed, blocked and empty searches are free.
Install and run a first search
pip install serpkite
export SERPKITE_API_KEY=skt_live_... # from app.serpkite.com/keys
from serpkite import SerpKite
sk = SerpKite() # reads SERPKITE_API_KEY
res = sk.search("best espresso machine", country="us", language="en")
for r in res.results[:5]:
print(r.position, r.title, r.link)
print("credits used:", res.meta.credits_used)
res.results holds the organic results in page order. Each one has position, title, link (the final URL, after Google’s redirects), domain and snippet. The models are pydantic, so res.model_dump() gives you a plain dict.
Choose the market: country, language and city
Google shows different results in different places. Pass country and language as two-letter codes, and location for city-level results:
res = sk.search("coffee roasters", country="de", language="de", location="Berlin,Germany", num=20)
if res.knowledge_graph:
print(res.knowledge_graph.title)
for q in res.people_also_ask or []:
print("People also ask:", q.question)
for s in res.related_searches or []:
print("Related:", s.query)
num asks for up to 100 results in one call. Besides results, the response carries the other parts of the page when Google shows them: knowledge_graph, answer_box, people_also_ask, related_searches, top_stories and places. See Localization for the codes and Search filters for date and domain filters.
Page through results
page walks the result pages. Stop when a page comes back empty, and drop repeats, because Google sometimes shows a URL on two pages:
seen, rows = set(), []
for page in range(1, 4):
res = sk.search("python web scraping tutorial", page=page)
if not res.results:
break
for r in res.results:
if r.link not in seen:
seen.add(r.link)
rows.append(r)
print(len(rows), "unique results")
Each page is a separate search. Pagination and depth explains when one call with a larger num is cheaper than several pages.
Run many queries at once with asyncio
AsyncSerpKite has the same methods as SerpKite, so you can send queries concurrently:
import asyncio
from serpkite import AsyncSerpKite
QUERIES = ["serp api", "google search api", "rank tracker api", "news api"]
async def main():
async with AsyncSerpKite() as sk:
responses = await asyncio.gather(*(sk.search(q, num=10) for q in QUERIES))
for q, res in zip(QUERIES, responses):
top = res.results[0] if res.results else None
print(f"{q!r}: {top.domain if top else 'no results'}")
asyncio.run(main())
For thousands of queries that don’t need an answer right away, batches cost half as much and call a webhook when they finish.
Save results to CSV
import csv
res = sk.search("project management software", num=20)
with open("results.csv", "w", newline="") as f:
w = csv.writer(f)
w.writerow(["position", "title", "link", "domain", "snippet"])
for r in res.results:
w.writerow([r.position, r.title, r.link, r.domain, r.snippet])
With pandas installed, pd.DataFrame([r.model_dump() for r in res.results]) gives you a DataFrame instead.
Markdown for LLMs
If the results go into a prompt, ask for Markdown. It keeps titles, links and snippets and uses far fewer tokens than JSON:
md = sk.search("how does retrieval augmented generation work", format="markdown")
print(md[:400])
format="markdown" returns a str. To put the text of the top pages in the same response, add include_content (see Page content).
Handle errors
The client retries rate limits, server errors and dropped connections on its own (max_retries, 2 by default). What’s left to handle are errors a retry won’t fix:
from serpkite import SerpKite, SerpKiteError, InsufficientCreditsError, RateLimitError
sk = SerpKite(max_retries=3)
try:
res = sk.search("best espresso machine", num=500)
except InsufficientCreditsError:
print("Out of credits: top up or raise the spend cap")
except RateLimitError as e:
print("Too many requests:", e.message)
except SerpKiteError as e:
print(e.status, e.code, e.message, e.request_id)
This example prints 400 invalid_request num must be between 1 and 100 …, because num is limited to 100. Every error carries a request_id to quote to support. The codes are listed in Errors.
Without the SDK
The API is plain HTTPS and JSON, so requests works too:
import os
import requests
resp = requests.post(
"https://api.serpkite.com/v1/search",
headers={"Authorization": f"Bearer {os.environ['SERPKITE_API_KEY']}"},
json={"q": "best espresso machine", "country": "us", "num": 10},
timeout=60,
)
resp.raise_for_status()
for r in resp.json()["results"]:
print(r["position"], r["title"], r["link"])
Coming from Google’s Custom Search JSON API
Google closed the Custom Search JSON API to new customers and shuts it down on January 1, 2027. If your Python code calls googleapis.com/customsearch/v1, you don’t have to rewrite it for the SDK: SerpKite’s /customsearch/v1 takes the same parameters and returns the same items[], so only the host and key change. See Migrate from Google CSE and the before-and-after scripts in SerpKite/custom-search-api-migration.
Next steps
- Python SDK reference: every method, including news, images, maps, reviews and Scholar.
- Google Search API: the response fields and pricing.
- Best SERP API: how SerpKite compares with SerpApi, Serper and others.
Last updated: 2026-10-25