Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
Release History
===============

Unreleased
----------

- Added `serpapi.AsyncClient` with async search, archive, account, locations,
image upload, and pagination support.
- Migrated the synchronous HTTP transport from Requests to HTTPX while keeping
the existing `serpapi.Client` API and request-option compatibility.
- Added explicit sync and async client lifecycle management, deterministic
concurrency tests, asyncio documentation, and a local HTTP benchmark.
- Updated the minimum supported Python version to 3.8 to match the SDK's CI
matrix and HTTPX requirements.

1.1.2 (2026-09-15)
------------------

Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ include README.md CONTRIBUTING.md HISTORY.md LICENSE
include .readthedocs.yaml
include scripts/check_docs_revision.py scripts/publish_docs.py
recursive-include tests *.py
recursive-include benchmarks *.py *.md
recursive-include docs *.py *.md *.txt *.css Makefile
include assets/serpapi-logo.svg assets/serpapi-icon.png
prune docs/_build
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ Query a vast range of data at scale, including web search results, flight schedu

## Installation

To install the `serpapi` package, simply run the following command:
Install the `serpapi` package with pip or add it to a uv project:

```bash
$ pip install serpapi
pip3 install serpapi
```

```bash
uv add serpapi
```

Python 3.8 or newer is required.

Please note that this package is separate from the legacy `serpapi` module, which is available on PyPi as `google-search-results`. This package is maintained by SerpApi, and is the recommended way to access the SerpApi service from Python.

## Simple Usage
Expand All @@ -36,6 +42,37 @@ print(results)

The `results` variable now contains a `SerpResults` object, which acts just like a standard dictionary, with some convenient functions added on top.

## Async Usage

Use `AsyncClient` in applications built on `asyncio`. Reuse one client so its
connection pool can serve all concurrent requests:

```python
import asyncio
import os

import serpapi


async def main():
async with serpapi.AsyncClient(
api_key=os.environ["SERPAPI_KEY"]
) as client:
results = await asyncio.gather(
client.search(engine="google", q="coffee"),
client.search(engine="google", q="tea"),
client.search(engine="google", q="pizza"),
)
print([result["search_metadata"]["id"] for result in results])


asyncio.run(main())
```

The synchronous `Client` and module-level helpers remain available. See the
[Asyncio Client guide](https://serpapi-python.readthedocs.io/en/latest/user_guide/asyncio.html)
for lifecycle, pagination, error-handling, and upload examples.

This example runs a search for "coffee" on Google. It then returns the results as a regular Python Hash.
See the [playground](https://serpapi.com/playground) to generate your own code.

Expand Down
35 changes: 35 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# HTTP client benchmark

This benchmark compares the previous `requests.Session` transport, the
synchronous HTTPX-backed `serpapi.Client`, and concurrent
`serpapi.AsyncClient` calls.

It runs against a local threaded server with a configurable response delay, so
it does not need an API key, consume searches, or mix client behavior with
internet and SerpApi server variance.

From the repository root, run:

```bash
uv run --with requests python benchmarks/http_clients.py
```

Or install the development package and benchmark-only dependency with pip:

```bash
pip3 install -e . requests
python benchmarks/http_clients.py
```

Each result is the median of three runs and includes the observed range. Client
construction and shutdown are excluded consistently. Change the workload with
`--count`, `--delay`, and `--repeats`. For example:

```bash
uv run --with requests python benchmarks/http_clients.py --count 50 --delay 0.1 --repeats 5
```

The sequential `requests` and HTTPX results show transport overhead under the
same workload. The async result demonstrates throughput when independent
I/O-bound calls overlap; it does not claim that one SerpApi search becomes
faster.
128 changes: 128 additions & 0 deletions benchmarks/http_clients.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Compare sequential requests with concurrent SerpApi AsyncClient calls.

The benchmark uses a local delayed HTTP server. It measures client-side
concurrency without consuming SerpApi searches or introducing internet and API
server variance.
"""

import argparse
import asyncio
import json
import statistics
import threading
import time
from contextlib import contextmanager
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import requests

import serpapi


class DelayedJSONHandler(BaseHTTPRequestHandler):
delay = 0.05

def do_GET(self):
time.sleep(self.delay)
payload = json.dumps({"search_metadata": {"status": "Success"}}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)

def log_message(self, format, *args):
pass


@contextmanager
def delayed_server(delay):
DelayedJSONHandler.delay = delay
server = ThreadingHTTPServer(("127.0.0.1", 0), DelayedJSONHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
host, port = server.server_address
yield f"http://{host}:{port}"
finally:
server.shutdown()
server.server_close()
thread.join()


def measure_requests(base_url, count):
session = requests.Session()
session.trust_env = False
try:
started = time.perf_counter()
for _ in range(count):
response = session.get(f"{base_url}/search", timeout=10)
response.raise_for_status()
return time.perf_counter() - started
finally:
session.close()


def measure_sync_client(base_url, count):
client = serpapi.Client(trust_env=False, timeout=10)
client.BASE_DOMAIN = base_url
try:
started = time.perf_counter()
for index in range(count):
client.search(q=f"query-{index}")
return time.perf_counter() - started
finally:
client.close()


async def measure_async_client(base_url, count):
client = serpapi.AsyncClient(trust_env=False, timeout=10)
async with client:
client.BASE_DOMAIN = base_url
started = time.perf_counter()
await asyncio.gather(
*(client.search(q=f"query-{index}") for index in range(count))
)
return time.perf_counter() - started


def summarize(label, timings):
median = statistics.median(timings)
spread = f"{min(timings):.3f}-{max(timings):.3f}s"
print(f"{label:<29} {median:.3f}s median ({spread})")
return median


def main():
parser = argparse.ArgumentParser()
parser.add_argument("--count", type=int, default=20)
parser.add_argument("--delay", type=float, default=0.05)
parser.add_argument("--repeats", type=int, default=3)
args = parser.parse_args()
if args.count < 1:
parser.error("--count must be at least 1")
if args.delay < 0:
parser.error("--delay must not be negative")
if args.repeats < 1:
parser.error("--repeats must be at least 1")

with delayed_server(args.delay) as base_url:
requests_timings = [
measure_requests(base_url, args.count) for _ in range(args.repeats)
]
sync_timings = [
measure_sync_client(base_url, args.count) for _ in range(args.repeats)
]
async_timings = [
asyncio.run(measure_async_client(base_url, args.count))
for _ in range(args.repeats)
]

summarize("requests.Session sequential:", requests_timings)
sync_median = summarize("serpapi.Client sequential:", sync_timings)
async_median = summarize("AsyncClient concurrent:", async_timings)
print(f"async vs sync speedup: {sync_median / async_median:.2f}x")


if __name__ == "__main__":
main()
7 changes: 4 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Run one of these commands in your terminal. Use `pip` to install into your Pytho
:::{tab-item} pip

```bash
pip install serpapi
pip3 install serpapi
```

:::
Expand All @@ -35,7 +35,7 @@ uv add serpapi

::::

The package requires Python 3.6 or newer.
The package requires Python 3.8 or newer.

## First Request

Expand Down Expand Up @@ -92,7 +92,7 @@ Use the same search parameter names as the [SerpApi API documentation](https://s
- [Output Formats](user_guide/output-formats.md) explains when to use JSON, Markdown, or HTML.
- [Migration Guide](user_guide/migrating-from-google-search-results.md) shows how to replace `google-search-results` with this package.
- [Parameters and Engines](user_guide/parameters-and-engines.md) explains search parameters and how to test them in the [SerpApi Playground](https://serpapi.com/playground).
- For scripts that collect results, see [pagination](user_guide/pagination.md), [timeouts and errors](user_guide/errors-and-timeouts.md), [request options](user_guide/request-options.md), and [account and locations](user_guide/account-and-locations.md).
- For concurrent applications and scripts that collect results, see the [Asyncio Client](user_guide/asyncio.md), [pagination](user_guide/pagination.md), [timeouts and errors](user_guide/errors-and-timeouts.md), [request options](user_guide/request-options.md), and [account and locations](user_guide/account-and-locations.md).
- For searches you retrieve later, selected response fields, and data retention settings, see [Async Search Archive](user_guide/async-search-archive.md), [JSON Restrictor](user_guide/json-restrictor.md), and [Zero Trace](user_guide/zero-trace.md).
- [Examples](examples/index.md) includes searches for web pages, AI answers, local businesses, products, travel, finance, trends, jobs, media, apps, and research.

Expand Down Expand Up @@ -132,6 +132,7 @@ user_guide/migrating-from-google-search-results
:caption: Advanced Usage

user_guide/async-search-archive
user_guide/asyncio
user_guide/threading
user_guide/multiprocessing
user_guide/zero-trace
Expand Down
108 changes: 108 additions & 0 deletions docs/user_guide/asyncio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
title: "Asyncio Client"
description: "Run concurrent SerpApi requests with AsyncClient and asyncio."
---

# Asyncio Client

`serpapi.AsyncClient` provides non-blocking versions of the SDK methods for
applications that use Python's `asyncio` event loop. This is separate from the
[Async Search Archive](async-search-archive.md), which is a SerpApi API feature
for retrieving a search after the server finishes processing it.

## Create and Close a Client

Use an async context manager so the connection pool is always closed:

```python
import asyncio
import os

import serpapi


async def main():
async with serpapi.AsyncClient(
api_key=os.environ["SERPAPI_KEY"],
timeout=20,
) as client:
results = await client.search(engine="google", q="coffee")
print(results["search_metadata"]["id"])


asyncio.run(main())
```

When a context manager does not fit the application lifecycle, call
`await client.aclose()` during shutdown. Create one client per event loop and
reuse it instead of constructing a client for every request.

## Run Independent Searches Concurrently

`asyncio.gather()` lets other requests make progress while one request waits
for network I/O:

```python
async def search_many(client):
return await asyncio.gather(
client.search(engine="google", q="coffee"),
client.search(engine="google", q="tea"),
client.search(engine="google", q="pizza"),
)
```

Concurrency improves throughput for independent I/O-bound requests. It does
not make one search finish faster, and each call still consumes a search from
the account.

## Other Async Methods

The async client supports the same endpoint parameters and response formats as
the synchronous client:

```python
async def inspect_account(client):
account = await client.account()
locations = await client.locations(q="Austin", limit=3)
archived = await client.search_archive(search_id="search-id")
upload = await client.upload_image("image.png")
return account, locations, archived, upload
```

The example above is an illustrative fragment and assumes it runs inside the
same async function and client context as the first example.

## Async Pagination

JSON search responses are `AsyncSerpResults` objects. Fetch one more page with
`await`, or iterate through several pages with `async for`:

```python
async def read_pages(results):
next_page = await results.next_page()

async for page in results.yield_pages(max_pages=5):
for item in page.get("organic_results", []):
print(item.get("title"))

return next_page
```

## Error Handling

Async methods raise the same SerpApi exceptions as synchronous methods:

```python
async def safe_search(client):
try:
return await client.search(engine="google", q="coffee")
except serpapi.TimeoutError:
print("The request timed out.")
except serpapi.HTTPConnectionError:
print("Could not connect to SerpApi.")
except serpapi.HTTPError as exc:
print(exc.status_code, exc.error)
```

See [Errors and Timeouts](errors-and-timeouts.md) and
[Request Options](request-options.md) for shared configuration details.
Loading