Between "I wonder who ranks for X" and opening a notebook to answer it sits about twenty minutes of boilerplate. Close that gap with a small CLI: type serp "best mechanical keyboard" --gl us, get the top results printed as a table, and pipe the same output to grep, jq-adjacent tooling, or a cron job. No framework, no config file — one file, standard library plus requests.
The whole thing
#!/usr/bin/env python3
"""serp.py — query a SERP API from the command line."""
import argparse
import json
import os
import sys
import requests
API_URL = "https://api.serpbase.dev/google/search"
def search(q: str, hl: str, gl: str, page: int, device: str | None) -> dict:
body = {"q": q, "hl": hl, "gl": gl, "page": page}
if device:
body["device"] = device # search endpoint only: default/pc/mobile
resp = requests.post(
API_URL,
headers={"X-API-Key": os.environ["SERPBASE_API_KEY"],
"Content-Type": "application/json"},
json=body,
timeout=30,
)
resp.raise_for_status()
data = resp.json()
if data.get("status") != 0:
sys.exit(f"API error {data.get('status')}: {data.get('error')}")
return data
def main() -> None:
p = argparse.ArgumentParser(description="Query Google results via a SERP API")
p.add_argument("query")
p.add_argument("--hl", default="en")
p.add_argument("--gl", default="us")
p.add_argument("--page", type=int, default=1)
p.add_argument("--device", choices=["default", "pc", "mobile"])
p.add_argument("--json", action="store_true", help="dump full response")
args = p.parse_args()
data = search(args.query, args.hl, args.gl, args.page, args.device)
if args.json:
print(json.dumps(data, ensure_ascii=False, indent=2))
return
for r in data.get("organic", []):
print(f"{r.get('rank', ''):>3} {r.get('title', '')}")
print(f" {r.get('link', '')}")
if __name__ == "__main__":
main()
Save it, chmod +x serp.py, export SERPBASE_API_KEY, and you have a working search tool. Params, the status/error envelope, and the organic field table are specified in the SerpBase search endpoint docs — the CLI adds a flag parser on top and nothing else.
The flags you'll actually use
| Flag | What for |
|---|---|
--json |
Full response for piping into other tools (`serp "..." --json \ |
{% raw %}--gl us / --hl en
|
Market and language; defaults match the API |
--page 2 |
Second page of results |
--device mobile |
See the mobile SERP when debugging layout-sensitive topics |
Why this beats a notebook for small questions
-
It composes.
serp "site:news.ycombinator.com serp api" --json | jq '.organic | length'answers "how many results" without editing any code. - It's cron-able. The same command in a scheduled task becomes a poor-man's monitor; add a timestamp and redirect to a file and you have snapshots.
- It fails loudly. A missing key raises immediately (env var lookup), and API errors exit non-zero with the code — so cron logs tell you what happened instead of silently writing empty output.
One habit worth keeping: the CLI never retries. For interactive use you'll see the error and rerun yourself; for automation, wrap the call in your own backoff so the retry policy lives in one place, not inside a "quick tool".
Cost discipline
Every invocation is one search request = 1 credit (errors don't charge). At $0.50/1k starting tier — with the $3/month Starter Boost covering 10,000 searches and 100 free searches for new accounts — casual CLI use is effectively free; volume belongs in your batch scripts, where you already control pacing.
FAQ
Why not argparse + urllib to drop requests entirely? You can; requests just makes the headers/timeout story shorter. The rest of the file has no other dependency.
Where do I put long-running features (caching, exports)? Not here. This file's whole value is that it does one request and prints one answer. Features belong in the module you import from scripts — the CLI stays a thin front door.
Windows? Works under python serp.py "query" in PowerShell; the | piping examples assume a POSIX shell, so on Windows use --json plus a redirect if you need to chain.
Steal the file, rename it, delete the flags you don't use — a tool you can hold in your head is the one you'll reach for.
Top comments (0)