Official Python SDK for the You.com API, published to PyPI as youdotcom. Since v3.0.0
(2026-08-06) it is hand-maintained, not generated — do not treat src/openapi/_hooks/
as authoritative.
Python >=3.10, src-layout, uv. Deps: httpx, httpcore, pydantic>=2.11. Go 1.22 mock
server for tests (tests/mockserver/, port 18080).
uv sync --dev
./scripts/run_tests.sh # starts mock server, pytest, cleanup
mypy src/youdotcom/
pylint src/youdotcom/ --rcfile=pylintrc
python scripts/check_drift.py # OpenAPI drift: 0=clean, 1=drift, 2=specs down
./scripts/run_performance_tests.sh --quick
scripts/publish.sh # build + publish (needs $PYPI_TOKEN)timeout_msis doing real work — without it httpx's 5s default raiseshttpx.ReadTimeouton most calls.- Key resolution:
api_key_autharg ->YDC_API_KEY-> legacyYOU_API_KEY_AUTH. Never default to""— an empty string raisesValueError; useos.getenv("YDC_API_KEY"). Youis a context manager and is not reusable afterwithexits; never close caller-supplied transports.include_domainscannot combine withexclude_domains/boost_domains(422).server_urlonly affectsapi.you.comendpoints; search/contents default toydc-index.ioand must be overridden per call.- Research helpers bound waits: 10 min default, 4h for
frontier. - Never enable
YOU_DEBUG=1in production; never commit debug logs.
CI gate (test.yml): pytest tests/ (excluding live/perf) + mypy + pylint errors-only
(tree is clean at 10.00/10). Test hygiene: leaked HTTP transports fail tests
(ResourceWarning-as-error). SemVer: breaking changes need MIGRATION.md + CHANGELOG.md.
- Add to method signatures —
_search_implandsearch_asyncmust both accept the new parameter (they share all logic via_build_search_request; never duplicate the body-construction block). - Add to
SearchRequestBody— the model field (with docstring), the TypedDict field, and theoptional_fieldsset inserialize_model. All three must be in sync. - Normalize at the SDK layer — if the API expects a specific casing, add a
_loweror_uppercall so callers can pass plain strings (the SDK normalizes to the casing the API expects). - Add tests — unit tests in
tests/test_search.py(MockTransport), plus a live test intests/test_live.pyif the parameter affects actual API behavior. - Update docs — README.md examples, USAGE.md if it's a user-facing feature. The
parameter table rows in
docs/sdks/{search,you}/README.mdanddocs/models/searchrequestbody.mdneed a new row that matches the surroundingTypecell notation exactly. - Run a surface sweep — when the new parameter supersedes, renames, or deprecates anything already exposed, every surface that mentioned the previous name must also teach the new one. See "When the new parameter deprecates or replaces an existing one" below for the full checklist.
SearchRequestBody.serialize_model uses a model_serializer(mode="wrap") that builds an
allowlist of optional fields. Fields with None values are dropped from the wire if
the field is in optional_fields (otherwise they serialize as JSON null). This means:
- Add new optional fields to the
optional_fieldsset, or they'll serialize asnullwhenNone. - To omit a field entirely by passing
None(e.g.language=None), it must be inoptional_fields. - To intentionally send JSON
null, do not add the field tooptional_fields. - Sub-models (
ExtractionHighlights,ExtractionFullPage,Extraction) use their ownmodel_serializer(mode="wrap")that dropsNonevalues so absent fields stay off the wire.
When adding a new user-facing type:
- Create the TypedDict first (in its own module if complex) — this is the type hint surface for callers who want to use dicts instead of models.
- Create the Pydantic model with
extra="forbid"on a strict base class — this catches unknown keys locally so callers fail-fast instead of routing to a 422. - Add type aliases (
Union[Enum, Literal[...]]) on TypedDict enum fields so callers can pass plain strings (the SDK normalizes at the method layer). - Export both from
src/youdotcom/models/__init__.pyusing the lazy-import pattern.
The extraction module demonstrates the recommended pattern for new typed API objects:
_StrictExtractionBase— Pydantic model withextra="forbid",populate_by_name=True,protected_namespaces=(),arbitrary_types_allowed=True. All sub-models inherit from this.- Sub-models (
ExtractionHighlights,ExtractionFullPage) — each has amodel_serializer(mode="wrap")that dropsNonevalues. - Top-level model (
Extraction) — has amodel_validator(mode="after")that enforces mutual-exclusion constraints (e.g.highlightsonly valid withextraction_mode=HIGHLIGHTS). model_validate()on the top-level model accepts both model instances and dicts — callers can pass{"extraction_mode": "highlights"}orExtraction(extraction_mode=...).
When adding a new parameter that should also flow through backward-compat shims:
- Add the parameter to
_shims.pySearchShim.__call__,unified, andunified_async. - Forward the parameter to
self._you._search_impl(...)orsearch_async(...). - The shim's
__call__emitsDeprecationWarningwithstacklevel=3so the warning reports the user frame, not the shim. - Add tests in
tests/test_shims.pyto verify the parameter is forwarded correctly.
New model exports go in three places in __init__.py:
TYPE_CHECKINGblock — import the class so type checkers see it.__all__list — add the name sofrom youdotcom.models import Xworks._module_mapdict — map the name to the source module path (e.g.".extraction") so runtime imports are lazy and don't cause circular imports.
When introducing a parameter that supersedes, renames, or removes anything already
shipped in SearchRequestBody, every surface that mentioned the previous name must be
swept. Missing a surface is the most common agent bug on this codebase — even one
stray doc referencing the old name reads as evidence of a half-finished migration.
MIGRATION.md— on every minor bump that ships a deprecation, add a## <old-version> → <new-version>section even if the old form still works. The section needs: a mapping table (old → new), before/after Python code blocks for both dict and model forms, a "no code change required" snippet when both still work, and a removal-in note (e.g. "removal targeted for 4.0.0").examples/api-example-calls.py— add or replace a runnable demo for the affected method matching the existing print/output pattern. Keep a paired "legacy" demo only when the deprecated form still works. Update theFUNCTIONSmenu in lockstep.tests/test_performance.py— if the parameter affects latency (network I/O, payload size, outgoing bytes per request), add a perf case mirroring the pattern of the analogous deprecated case. Use the samecreate_timing_client("post_/v1/search")measure_sdk_call(...)+ALL_METRICS.append(...)recipe.
- New model pages — if a new Pydantic model was added, create
docs/models/<modelname>.mdwith a description, a runnable example (both dict and model forms), and a fields table. Matchdocs/models/extraction.md's format exactly. - Existing model pages — fields added to existing models (
Contents.highlights, etc.) get a row indocs/models/<thatmodel>.md's field table; the model's prose description is rewritten so it reads "ifextractionwas enabled (formerlylivecrawl)" rather than the old phrasing. - Docstring carry-over — narration on
Contents/WebResult.contents/NewsResult.contentsand similar fields is rewritten with(formerly X)when the behavior is identical between old and new parameters. - CHANGELOG.md and README.md — read together: every feature on the README usage section should match a CHANGELOG entry, and the CHANGELOG should mention the deprecation/removal note if any.
USAGE.md— if usage prose mentions the deprecated parameter, update the prose to teach the new parameter and demote the old one with a(formerly X)note.
Skip a step only when that surface legitimately does not apply (no MIGRATION section on a non-deprecating change). Document any skip in the PR body.
The 8-step surface sweep above catches most rename/deprecation oversights, but the categories below still slipped through on PR #40. Codify them so they don't reopen the same review threads on the next migration.
The same field has two docstrings in SearchRequestBody: one on the TypedDict
field and one on the Pydantic field. Editing only the Pydantic string leaves the IDE
surface (via the TypedDict) stale, and vice versa. Always edit both in lockstep after
any behavior or wording change.
The two docstrings live in different physical locations — keep the shape in mind when editing:
- On the
TypedDict, the r-string sits directly below the type annotation, indented one more level than the field name. - On the Pydantic model class, the r-string sits directly below the
Field(..., description=...)line (or, when no Field is used, below the type annotation itself). TheField(description=...)and the r-string are independent help surfaces — one feeds the generated OpenAPI / JSON schema, the other feeds IDE/help. Both should describe the same thing in the same words; a missing r-string silently breaks IDE/help while leaving the API schema intact.
A model's module docstring at the top of src/youdotcom/models/<x>.py often describes
the SDK-layer behavior of a parameter (models/extraction.py's crawl_timeout
stanza is an example). When that parameter's behavior changes, the module docstring
is also stale even if the field-level docstrings are not. Re-read the full module
docstring from top to bottom after any behavior edit.
docs/models/webresult.md, docs/models/newsresult.md, and similar *result.md
pages have contents rows that describe the gating parameter for livecrawl-family
behavior — phrased in terms of the deprecated name (e.g. "Contents of the page if
livecrawl was enabled"). When the gating parameter is renamed or deprecated, those
rows must read "if extraction was enabled (formerly livecrawl)" instead. Step 5
above lists "existing model pages" but does not call out *result.md files; grep
docs/models/*result.md for the deprecated name on every rename sweep.
On the same sweep, also diff every Type cell in docs/models/*result.md against
the source annotations in the matching src/youdotcom/models/*result.py. The
Type-cell traps below (date↔datetime, Optional↔non-Optional, etc.) apply here too —
and a casual sweep that only re-reads contents rows will miss unrelated rows like
snippets, page_age, or thumbnail_url going out of sync with the source.
Treat all *result.md files as part of the sweep even when the contents row was
the only one written in prose.
The same parameter appears in docs/sdks/you/README.md, docs/sdks/search/README.md,
docs/models/searchrequestbody.md, and the TypedDict + Pydantic field docstrings
in src/youdotcom/models/. The Description cells in those four locations must read
identically (the Search sub-SDK table may add a no-op note when the underlying
endpoint genuinely strips the field, but otherwise the wording is fixed). After
updating one, the others must also be updated; divergent wording is a near-guaranteed
review comment.
The Type column of docs/models/<model>.md is the caller-facing paraphrase of the
model's actual annotation. Optional[List[str]] becomes Optional[List[*str*]], not
List[*str*]. Eyeball every row in the touched model page for the same
cell-vs-annotation drift; one botched cell sits there forever otherwise.
A handful of broken mappings have slipped past this rule even when the implementing agent skimmed the rows — call them out by name so the sweep doesn't have to discover them anew:
Optional[X]↔X. The annotation determines whether the field is optional; the cell must mirror that with or withoutOptional[...].datetime↔date.datestrips time-of-day from the timestamp. If the source annotation isdatetime(orOptional[datetime]), the cell must link to thedatetime-objectsanchor ofdatetime.html, notdate-objects.bool↔intfor predicate / flag fields.str↔intfor ID fields.
Eyeball every row in the touched model page for these traps, not just the rows whose prose changed in the same edit.
A ## Example Usage block in docs/**/*.md (or README.md / USAGE.md) is a
contract with the reader that copy-pasting the snippet will work on a fresh checkout.
Any snippet that calls you.search(...) without constructing a You instance — or
without showing the import lines that back the snippet — fails that contract with
NameError: name 'you' is not defined. Always include: (1) the import statements,
(2) client construction via You(api_key_auth=os.getenv("YDC_API_KEY")), and (3) a
with ... as you: context for any method calls. Match the pattern used elsewhere in
docs/sdks/* before committing the snippet.
Any snippet that triggers a network round-trip (you.search, you.contents,
you.research, anything with crawl_timeout or extraction_mode="full_page")
must pass an explicit timeout_ms to You(...). httpx's 5-second default raises
ReadTimeout on most real calls (the "must-know" line at the top of this file
already calls this out). A round-trip ready-to-run snippet without timeout_ms
is a copy-paste footgun. Use 60_000 ms (60 s) as the default for snippet
examples; live tests can vary by call shape.
When renaming or deprecating a parameter that already lives on
SearchRequestBody, update both the TypedDict field docstring and the
Pydantic field docstring. They are not auto-synced — IDE/type-helper parity fails
otherwise. The opening of each docstring should start with "Deprecated; use
<new-name> instead." so callers browsing the IDE surface see it explicitly.
The same wording is used in the docs tables (docs/sdks/you/README.md,
docs/sdks/search/README.md, docs/models/searchrequestbody.md) — keep all
four surfaces in lockstep.
When the helper that detects a conflict also has reason to warn (e.g. an extraction
vs. deprecated livecrawl conflict, plus a DeprecationWarning on livecrawl),
order the two reactions as raise first, warn second. Pytest and many lint runners
promote DeprecationWarning to an error via -W error::DeprecationWarning; a
warning-first ordering converts the warning into an exception that fires before the
conflict raise can run, masking the underlying 422 the caller is hitting. The
canonical ordering lives in _build_search_request; preserve it on future edits.
When a parameter combination is invalid on the wire but you want default callers to work without 422 errors:
- Detect the conflict in
_build_search_request(the shared helper). - Strip the conflicting parameter from the body (set to
Noneso the serializer drops it). - Warn with
UserWarningif the caller set a non-default value — default callers stay silent. Usestacklevel=4since the helper adds a frame. - Document the rule in the method docstring and the parameter docstring.
- Add tests: one for the default case (silent strip), one for explicit non-default (warns
- strips), and one for the non-conflicting case (keeps the value).
- Unit tests use
httpx.MockTransport— no mock server needed. The_capture()context manager intests/test_extraction.pyis the standard pattern: yield(you, captured)wherecaptured["body"]has the JSON that went over the wire. - Live tests require
YDC_API_KEYand run against the real API. Mark with@requires_api_keyand keep them minimal — they verify the SDK doesn't crash, not the API behavior. - Mock server (
tests/mockserver/) is a Go app that serves static JSON responses. It's used byrun_tests.shbut is not required for unit tests (which use MockTransport). - Tests that depend on the mock server (port 18080) will fail with
Connection refusedif Go is not installed. State which checks were skipped and why.
Live tests (@requires_api_key) are not homage. They are the only runtime check that
the SDK + API pair still match. Write them so a contract break fails loudly:
A live test whose body is if result.contents: <loop with no failing assertion>
silently passes when every result returns no contents. Instead accumulate the
observed matches into a list and assert the list is non-empty:
content_seen = [
(r.contents.html, r.contents.markdown)
for r in res.results.web
if r.contents and (r.contents.html is not None or r.contents.markdown is not None)
]
assert content_seen, "Expected at least one result with contents.html and/or contents.markdown"The same <x>_seen = [...filtered list...]; assert <x>_seen pattern applies to
contents.highlights, results.news[].contents, and any "should be present"
assertion over a collection.
The server returns empty strings for absent content, so
assert item.contents.markdown or item.contents.html fails even when both fields
are present but empty. Use ... is not None or ... is not None when checking
API-returned string fields. Truthiness is not a presence check for server-returned
strings.
The function name should describe what the body asserts. A test named
test_extraction_highlights_default_no_subkeys that constructs
Extraction(extraction_mode="full_page") is a bug — rename it to
test_extraction_full_page_default_no_subkeys. Test-name vs body divergence is
the smoking-gun reviewer signal that the suite was written against an earlier
version of the code.
Any input(...) prompt in examples/, scripts/, or a CLI surface that asks for
an API key, OAuth token, password, or other secret must use
getpass.getpass(...) rather than the builtin input. The plain input echoes
every keystroke to the terminal — into terminal recorders, screen-shares, and
shell-history candidates. getpass.getpass suppresses the echo. Add import getpass near the top of the file alongside the other stdlib imports and apply
to every interactive secret prompt — not just the first one.