# 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`](https://serpkite.com/docs/sdks) package. A runnable copy of the examples lives in [SerpKite/google-search-api](https://github.com/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

```bash
pip install serpkite
export SERPKITE_API_KEY=skt_live_...   # from app.serpkite.com/keys
```

```python
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:

```python
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](https://serpkite.com/docs/localization) for the codes and [Search filters](https://serpkite.com/docs/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:

```python
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](https://serpkite.com/docs/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:

```python
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](https://serpkite.com/docs/batch) cost half as much and call a webhook when they finish.

## Save results to CSV

```python
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:

```python
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](https://serpkite.com/docs/include-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:

```python
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](https://serpkite.com/docs/errors).

## Without the SDK

The API is plain HTTPS and JSON, so `requests` works too:

```python
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](https://serpkite.com/docs/guides/migrate-from-google-cse) and the before-and-after scripts in [SerpKite/custom-search-api-migration](https://github.com/SerpKite/custom-search-api-migration).

## Next steps

- [Python SDK reference](https://serpkite.com/docs/sdks): every method, including news, images, maps, reviews and Scholar.
- [Google Search API](https://serpkite.com/apis/google-search-api): the response fields and pricing.
- [Best SERP API](https://serpkite.com/compare/best-serp-api): how SerpKite compares with SerpApi, Serper and others.