DEV Community

dodou
dodou

Posted on

serp: a 60-Line Python CLI for Google Search Results

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()
Enter fullscreen mode Exit fullscreen mode

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)