What this skill does
Installs and drives pplx-srch-sdk, the public Python SDK for the Search API: pplx_srch_sdk.search.web for live web search, pplx_srch_sdk.search.web_many for concurrent fan-out over many queries, pplx_srch_sdk.content.snippets for query-relevant page excerpts.
The sync facade covers most work; pplx_srch_sdk.AsyncPplxClient exposes the same surface async.
Install and auth
pip install pplx-srch-sdk
Python >= 3.12 on Linux (x86_64, aarch64) and macOS. Package and import share the name: pip install pplx-srch-sdk, then import pplx_srch_sdk.
Get an API key at https://www.perplexity.ai/account/api and export it:
export PERPLEXITY_API_KEY=pplx-...
Import succeeds without a key; a missing key raises AuthenticationError at first use (pitfall 8).
Web search
import pplx_srch_sdk
hits = pplx_srch_sdk.search.web("rust async runtimes", limit=5)
for hit in hits:
print(hit.title, hit.url)
search.web returns a bare list[WebHit] (pitfall 1).
Keep queries short keyword phrases, usually 2-5 meaningful words, one topic per query; no quote marks, site:, or boolean AND/OR - use kwargs like domains=[...] and excluded_domains=[...] instead.
Break multi-entity questions into separate single-entity queries and send them through web_many, not one long combined query.
A list passed as the first argument is reformulations of ONE query, up to 10, merged into a single result list (pitfall 2).
On non-trivial searches, add intent= - one short sentence stating what the search should find or verify; it sharpens which text comes back for each hit without changing which pages are found.
Full kwarg table (domains, country, date bounds, intent, token budgets): references/search.md.
Fan-out (many independent queries)
results = pplx_srch_sdk.search.web_many(
[
"yellowstone national park visitor numbers 2025",
"zion national park entrance fee 2026",
"acadia national park camping reservations",
],
limit_per_query=10,
)
for r in results:
if r.ok:
print(r.spec["query"], "->", len(r.result), "hits")
else:
print(r.spec["query"], "failed:", r.error)
web_many runs one independent search per query with bounded concurrency (default 5) and returns one FanoutResult per query, in input order.
Read r.ok / r.spec / r.result / r.error (pitfall 3); one failed query does not fail the batch.
Queries can be plain strings or per-query kwargs dicts ({"query": ..., "domains": [...]}); extra kwargs on the call apply to every query.
Mechanics plus the pplx_srch_sdk.utils helpers (partition, flatten, dedup, JSONL artifacts): references/fanout.md.
Snippets
snips = pplx_srch_sdk.content.snippets(
query="what's new in python 3.13",
urls=[
"https://docs.python.org/3/whatsnew/3.13.html",
"https://example.com/missing",
],
)
for s in snips:
print(s.url, s.error or (s.text or "")[:200])
One SnippetResult per input URL, in input order.
Check error on every result before trusting text (pitfall 4).
Use text, not content; elided regions inside text are marked with ….
Token budgets and URL limits: references/content.md.
Result shapes
search.web(...)->list[WebHit]; each hit has{url, title, domain, snippet, date?, last_updated?}.snippetis the text field for the hit and is an empty string when no text is available;dateis the publication date andlast_updatedthe last-modified date.- Typed records support attribute reads (
hit.url), mapping access (hit["url"],{**hit},hit.keys()), anddict(hit)/hit.to_dict()for JSON-serializable rows. Missing optional fields read asNone; the mapping view contains only populated fields. search.web_many(...)->list[FanoutResult]with.ok/.spec/.result/.error(pitfall 3; details in references/fanout.md).content.snippets(...)->list[SnippetResult]; each has{url, text?, tokens_count?, error?}.
Errors
Every SDK exception derives from pplx_srch_sdk.PplxSdkError; API failures raise subclasses of pplx_srch_sdk.APIError: AuthenticationError (401), ForbiddenError (403), NotFoundError (404), NotAcceptableError (406), TimeoutError (408, client-side - ~10 s for search, 30 s for snippets, not configurable), RateLimitError (429), BadRequestError (400), ValidationError (422), InternalServerError (5xx), ConnectError (network failure).
TimeoutError here is pplx_srch_sdk.TimeoutError, not the builtin: except TimeoutError: will not catch it - catch APIError or import it explicitly; it is excluded from import *.
Every APIError also carries e.status_code (int) and e.body (str); branch on e.status_code, not the message text.
import time
import pplx_srch_sdk
for attempt in range(3):
try:
hits = pplx_srch_sdk.search.web("query", limit=5)
break
except pplx_srch_sdk.RateLimitError:
if attempt == 2:
raise
time.sleep(2**attempt) # bounded backoff: 1 s, then 2 s
except pplx_srch_sdk.APIError as e:
print(e.status_code, e.body) # e.g. 400 and the server's error detail
break
The SDK never retries or throttles - each call sends exactly one billable HTTP request. On RateLimitError, lower concurrency, wait, then re-dispatch only the failed specs.
Inside web_many, per-query API errors land on FanoutResult.error instead of raising; a missing API key still raises AuthenticationError immediately, before any query is dispatched.
Async
import asyncio
import pplx_srch_sdk
async def main():
async with pplx_srch_sdk.AsyncPplxClient() as client:
hits = await client.search.web("python 3.13 release notes", limit=10)
print(len(hits), hits[0].url)
asyncio.run(main())
Drop to AsyncPplxClient for a custom concurrency model or long-lived concurrent state; there is no async web_many - use pplx_srch_sdk.utils.fanout with client.search.web (see references/async.md).
Reuse one client per script (pitfall 6).
Top pitfalls
search.webreturns a barelist[WebHit]. There is no.resultswrapper or response envelope:hits[0].urlworks directly,hits.resultsis anAttributeError.- A list passed to
web()is reformulations of ONE query, not a batch. Up to 10 variants merge into a single result list, andlimitcaps that whole merged set, not each variant. N distinct questions =web_manywithlimit_per_query. FanoutResultexposesspec/result/error/ok.r.resultis the hit list andr.specthe request kwargs; there is no.requestattribute. Always branch onr.ok- per-query failures ride the batch asr.error, they do not raise.- A successful
content.snippetscall does not mean every URL succeeded. Per-URL failures setresults[i].errorand leavetextunset; checkerroron each result before usingtext. - Date filter kwargs use MM/DD/YYYY strings, not ISO dates (e.g.
published_after_date="7/1/2026"); padding is optional.published_after_date/published_before_datecannot be combined withrecency_filter; the server rejects these combinations. - Do not construct one
AsyncPplxClientper coroutine insideasyncio.gather. That leaks connections - reuse one client, or stay onweb_many/pplx_srch_sdk.utils.fanout. - A conflicting
limitalongsidelimit_per_queryonweb_manyraisesTypeError. Equal values are accepted but redundant; preferlimit_per_queryalone, which forwards aslimit=to each single search. - A missing key fails at call time, not import time.
import pplx_srch_sdksucceeds withoutPERPLEXITY_API_KEY; the first sync-facade call raisesAuthenticationError: [401] No API key configured. Set the PERPLEXITY_API_KEY environment variable, andAsyncPplxClient()raises the same error eagerly at construction - wrap client creation, not just the awaited calls (as examples/async_client.py does).
Reference index
- references/search.md - read when composing queries or filters: full
search.webkwarg table, query-style rules, date filters,WebHitshape. - references/fanout.md - read when running many queries:
web_manymechanics,FanoutResulthandling,pplx_srch_sdk.utilshelpers for artifacts and dedup. - references/content.md - read when excerpting known URLs:
content.snippetstoken budgets, per-URL error handling. - references/async.md - read when using
AsyncPplxClientorpplx_srch_sdk.utils.fanoutdirectly.
Runnable examples (each needs PERPLEXITY_API_KEY exported):
- examples/web_search.py - one-off web search with filters and typed error handling.
- examples/fanout_research.py - fan out queries, split successes from errors, dedup, write JSONL artifacts.
- examples/snippets_verify.py - search, then verify hits with query-relevant snippets.
- examples/async_client.py - async client with bounded custom fan-out.