Blog
 / 
API
October 9, 2026

How to Parse Search Results JSON in Python with the You.com Web Search API

How to Parse Search Results JSON in Python with the You.com Web Search API

TLDR: Parsing a search API response in Python is two moves, load the body with json.load, then walk the results buckets defensively, and the failure modes all live in the walking. This guide covers a defensive traversal of the web and news buckets, mapping hits into your own dataclass, normalizing missing fields, and testing the parser against recorded fixtures. Working Python against the You.com Web Search API, with the failure modes that crash naive parsers in production.

Every search-driven application has a parsing layer, even the ones that pretend not to, and most of them are written twice: once as a quick json.loads(...)[0] and again after the first empty response or missing field takes the process down. The Web Search API from You.com returns clean, predictable JSON, which is exactly why this guide still exists: predictable input is the reason a small, well-tested parsing layer is worth writing once and keeping forever. The Web Search API guide documents the full request surface, the official field reference is in What Is in a Web Search API JSON Response?, and the Python json module documentation covers the loading half.

How Do You Load the Response Safely?

The response is a JSON document over HTTP, so the load step is the standard library, nothing exotic, and the safety lives in the boundaries:

import json
import os
import urllib.request


def search(query: str, count: int = 5) -> dict:
    req = urllib.request.Request(
        "https://ydc-index.io/v1/search",
        data=json.dumps({"query": query, "count": count}).encode(),
        headers={
            "Content-Type": "application/json",
            "X-API-Key": os.environ["YOUCOM_API_KEY"],
        },
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.load(resp)

Two boundaries matter. The timeout exists because a parse cannot fail gracefully on a response that never arrives, so the network call carries its own bound. And the API key reads from the environment rather than a literal, which keeps it out of source control, logs, and exception tracebacks. The json module is the only parser in the picture: never eval on a response body, and never a third-party parser where the standard one is exact.

How Do You Traverse the Results Structure?

The results arrive in typed buckets, web and news, and the traversal walks both without assuming either exists. The defensive form is one line per level:

def all_hits(payload: dict) -> list:
    buckets = payload.get("results", {}) or {}
    hits = []
    for bucket in ("web", "news"):
        for hit in buckets.get(bucket, []) or []:
            hits.append({
                "kind": bucket,
                "url": hit.get("url", ""),
                "title": hit.get("title", ""),
                "text": (hit.get("snippets") or [hit.get("description", "")])[0],
                "page_age": hit.get("page_age", ""),
            })
    return hits

Every .get default is a production decision, not paranoia. A missing results object, an empty news bucket, or a web hit without a snippets list are all data conditions a search engine can legitimately produce, and the difference between "handle it" and "crash on it" is whether the traversal assumed them away. Note the snippet fallback: web results carry keyword fragments in snippets, and a missing or empty list falls back to the description, so the parser never hands your code an empty string when the engine had something to say. Also note what the traversal does not do: it does not sort by score, filter by age, or drop fields. Parsing layers that also decide are the ones that get rewritten twice.

How Do You Map JSON into Python Types?

Raw dicts leak into every function signature and rot the codebase, so the parsing layer ends at a dataclass. The class makes the schema explicit and gives every downstream function a real type:

from dataclasses import dataclass


@dataclass
class SearchResult:
    kind: str
    url: str
    title: str
    snippet: str
    page_age: str


def to_results(payload: dict) -> list:
    results = []
    for hit in all_hits(payload):
        results.append(SearchResult(
            kind=hit["kind"],
            url=hit["url"],
            title=hit["title"],
            snippet=hit["text"],
            page_age=hit["page_age"],
        ))
    return results


for r in to_results(search("python json parsing")):
    print(r.kind, "|", r.page_age or "no age", "|", r.url)

The mapping is where normalization belongs, because it is the last place the raw shape is visible. The page_age passes through as a string rather than a parsed date, which is deliberate: age formats vary by engine and era, and a parser that guesses the format couples your whole application to a guess. Keep the string, let the consumer decide, and if a consumer needs dates, parse there with tests.

How Do You Test a Parsing Layer?

With recorded fixtures, never live calls. Save a few real response bodies to files, load them in tests, and assert the traversal and mapping against them:

def test_parser_ignores_missing_fields():
    fixture = json.load(open("fixtures/search_response.json"))
    results = to_results(fixture)
    assert results, "a real response must yield at least one result"
    for r in results:
        assert r.url, "url is the one field the parser may never drop"
        assert isinstance(r.page_age, str)


def test_parser_handles_empty_buckets():
    empty = {"results": {}}
    assert to_results(empty) == []
    assert to_results({}) == []

Two tests cover most parser rot. The fixture test pins the real shape: when the API response changes, the test fails with the fixture in hand instead of failing in production with a traceback. The empty tests pin the contract that missing buckets are normal, not exceptions. Both are pure functions, so they run in milliseconds, which is the difference between a parsing layer you keep tested and one you keep hoping.

What Failure Modes Break Parsers in Production?

Four account for nearly everything, and each has a detection.

KeyError on a hard-indexed field, hit["snippet"] in older code, is the most common crash, and the fix is the traversal above rather than a try block. Detection: the fixture tests, because a recorded response with a missing field fails in CI instead of in production. The empty-list-or-None confusion is sneakier: buckets.get("web", []) returns None, not the empty default, when the key exists with a null value, and the or [] in the traversal closes exactly that gap. Detection: a fixture with explicit nulls, which is worth recording once and keeping forever. Type surprises are third: page_age as a string today could be a number tomorrow, and the dataclass assertion in the fixture test is what catches the drift. Last, truncated or empty output is the silent one: a parser that yields zero hits looks identical to an API outage until you log the bucket counts. Detection: log len(web) and len(news) per response and alert on a sudden stream of zeros, because "the API returned nothing" and "our parser dropped everything" need to be distinguishable from each other at 3 a.m.

Related Guides

FAQ

How do I parse search results JSON in Python? Load the response body with json.load, walk the web and news result buckets with .get calls and defaults, and map each hit into your own dataclass. The traversal is defensive because missing buckets and empty snippet lists are normal data conditions, not exceptions.

What Python library do I need to parse the response? None beyond the standard library. The json module is exact for this job: json.load for the response body, json.dumps for request bodies. A response body is untrusted input, so the standard parser beats eval or hand-rolled parsing every time.

How do I handle missing fields in search results? Default them explicitly at the traversal layer: .get with a fallback for every optional field, an or [] for lists that can arrive null, and a final dataclass mapping that makes the normalized schema visible. Never let a missing field decide whether your process lives.

How do I test a parsing layer without spending API calls? Record real response bodies to fixture files, then test the parser as a pure function against them: one test pinning the real shape, one test with empty and null buckets. The fixtures run in milliseconds and fail in CI, which is where schema drift should fail.

Frequently Asked Questions

Load the response body with json.load, walk the web and news result buckets with .get calls and defaults, and map each hit into your own dataclass. The traversal is defensive because missing buckets and empty snippet lists are normal data conditions, not exceptions.

None beyond the standard library. The json module is exact for this job: json.load for the response body, json.dumps for request bodies. A response body is untrusted input, so the standard parser beats eval or hand-rolled parsing every time.

Default them explicitly at the traversal layer: .get with a fallback for every optional field, an or [] for lists that can arrive null, and a final dataclass mapping that makes the normalized schema visible. Never let a missing field decide whether your process lives.

Record real response bodies to fixture files, then test the parser as a pure function against them: one test pinning the real shape, one test with empty and null buckets. The fixtures run in milliseconds and fail in CI, which is where schema drift should fail.

    Share Article:

  1. LI Test

  2. LI Test

Related resources.

How to Feed Real-Time Data to an LLM with the You.com Web Search API

October 9, 2026

Blog

How to Retrieve Version-Specific Library Docs with the You.com Web Search and Contents APIs

October 9, 2026

Blog

How to Work Around LLM Knowledge Cutoffs with the You.com Web Search API

October 9, 2026

Blog

How to Get Search Results Programmatically with the You.com Web Search API

October 8, 2026

Blog

How to Build a Documentation Search API with the You.com Web Search API

October 8, 2026

Blog