AI Agent Web Search: Build a Web Search Agent in Python (2026)
A runnable 2026 tutorial: give an AI agent web search in Python with the Claude web_search server tool. Read citations, cap cost with max_uses, filter domains, and go multi turn.

On this page
> Quick answer (July 2026): The fastest way to give an AI agent web search in Python is the Claude API's built in web_search server tool. You add one entry to the tools array, Claude decides when to search, runs the searches server side, and returns an answer with cited sources. It costs $10 per 1,000 searches plus normal token costs (Anthropic, 2026). This tutorial builds a runnable web search agent, reads its citations, bounds its cost, and turns it into a multi turn agent.
This is a code first, copy paste tutorial. Everything below runs against the
Anthropic API with the
Python SDK. If you have never wired a tool onto a Claude agent before, read give your agent custom tools first, then come back.
An agent that cannot see the live web is stuck at its training cutoff. It cannot tell you today's release, a current price, or what shipped this morning. The good news: as of 2026 you do not have to build a scraper, a search index, or a retrieval pipeline to fix that. Anthropic runs the search for you and hands Claude cited results.
What you will build
A small Python program where you ask a question, the agent decides on its own whether it needs to search, runs one or more web searches, and answers with links to its sources. Then you will bound its cost, filter which domains it may read, and extend it to hold a conversation.
Prerequisites
- Python 3.10 or newer
- An Anthropic API key (set
ANTHROPIC_API_KEYin your shell) - Five minutes
Install the SDK
pip install anthropic
export ANTHROPIC_API_KEY="sk-ant-..."
The official SDK source lives at github.com/anthropics/anthropic-sdk-python if you want to read along.
The smallest possible web search agent
The web_search tool is a server tool: you declare it, and Anthropic executes the search on its side, feeds the results back to Claude, and loops until Claude is ready to answer. You do not write a search function.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "What changed in the EU AI Act timeline in 2026? Cite your sources."}
],
tools=[
{"type": "web_search_20250305", "name": "web_search", "max_uses": 5}
],
)
for block in response.content:
if block.type == "text":
print(block.text)
Run it. Claude notices the question needs current information, issues one or more searches, and prints an answer grounded in what it found. Ask it something stable instead ("what is a hash map?") and it will answer without searching at all. That decision is the whole point of an agent: it picks the tool when the tool helps.
web_search_20250305 is the basic version. Two newer versions exist as of 2026, web_search_20260209 (adds dynamic filtering, so Claude filters results with code before they hit the context window) and web_search_20260318 (adds response inclusion control). Start with the basic one; upgrade when token cost on search heavy runs becomes a concern.
Read the citations
Every web search answer carries citations, and if you show search output to users you are expected to surface them. They hang off each text block:
def print_answer_with_sources(response):
sources = []
for block in response.content:
if block.type == "text":
print(block.text, end="")
for citation in getattr(block, "citations", None) or []:
sources.append((citation.title, citation.url))
print("\n\nSources:")
for title, url in dict((u, t) for t, u in sources).items():
print(f"- {sources and ''}{url}")
# usage
print_answer_with_sources(response)
Each citation gives you url, title, and up to 150 characters of cited_text. Those citation fields do not count toward your token bill, so rendering them is free.
Bound the cost and scope
Two things you will want in any real agent: a cap on how many searches a single request may run, and a limit on which sites it may read.
RESEARCH_TOOL = {
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3, # hard cap per request
"allowed_domains": ["arxiv.org", "anthropic.com"], # only read these
# "blocked_domains": ["pinterest.com"], # or ban some (never use both at once)
"user_location": { # localize results
"type": "approximate",
"country": "US",
"timezone": "America/New_York",
},
}
max_uses is your cost governor. Simple factual questions use one to three searches; open ended research can use ten or more, so an uncapped research agent can quietly run up searches. At $10 per 1,000 searches (Anthropic, 2026), max_uses: 3 keeps a latency sensitive lookup to three cents, worst case. Use allowed_domains or blocked_domains, never both in the same tool, or the API returns a 400.
Turn it into a multi turn agent
A one shot answer is not an agent you can talk to. To hold a conversation, append the assistant's reply back into messages and keep going.
messages = [{"role": "user", "content": "Find the latest Claude model and its context window."}]
def run_turn(messages):
response = client.messages.create(
model="claude-sonnet-5",
max_tokens=1024,
messages=messages,
tools=[RESEARCH_TOOL],
)
# The API can pause a long search turn. Resend the assistant message to continue.
while response.stop_reason == "pause_turn":
messages.append({"role": "assistant", "content": response.content})
response = client.messages.create(
model="claude-sonnet-5", max_tokens=1024, messages=messages, tools=[RESEARCH_TOOL]
)
return response
response = run_turn(messages)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": "How does that compare to the previous version?"})
response = run_turn(messages)
The gotcha most tutorials miss: each search result carries an encrypted_content field, and you must send the assistant's content blocks back exactly as you received them, encrypted content included. Do not rebuild the assistant message by hand or strip fields to "clean it up", the next request will fail with a 400 validation error. Passing response.content straight back, as above, is the safe path.
Built in tool versus rolling your own
You do not have to use Anthropic's search. You can define a client side tool and call any search API yourself, for example Tavily or the Brave Search API. That is the same client tool loop from the custom tools tutorial: Claude emits a tool_use, you run the search, you return a tool_result. Here is the decision, honestly:
Scroll to see more
| Approach | What you implement | Cost model (2026) | Citations | Best when |
|---|---|---|---|---|
Built in web_search tool | one line in tools | $10 per 1,000 searches + tokens | automatic, source cited | you want working search today |
| Your own tool + a search API | the tool loop and the API calls | your search vendor's bill + tokens | you build them yourself | you need a specific index, an existing search contract, or on prem data |
The built in tool wins for most agents because search, iteration, and citations are already wired. Reach for a custom tool when you need a source Anthropic does not search, such as your own document store or an internal wiki.
Doing it inside the Claude Agent SDK
If you are building on the Claude Agent SDK rather than raw Messages calls, you get the same capability as one of the SDK's built in tools. You allow it and the agent uses it inside its own loop:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
model="claude-sonnet-5",
system_prompt="You are a research assistant. Search the web when a fact might be current, and always cite links.",
allowed_tools=["WebSearch"],
)
async for message in query(prompt="What did Anthropic ship this week? Cite sources.", options=options):
print(message)
asyncio.run(main())
The raw Messages approach in Steps 2 to 5 gives you the most control over cost and domains; the Agent SDK gives you a longer running loop with less plumbing. Pick based on how much of the loop you want to own.
Verify it works
Run this one file end to end. It should print an answer plus at least one real URL. If you see sources, your web search agent is live:
r = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": "What is today's date and one headline from this week? Cite the source."}],
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 2}],
)
cites = [c.url for b in r.content if b.type == "text" for c in (getattr(b, "citations", None) or [])]
print("citations found:", len(cites))
assert len(cites) >= 1, "no citations: check ANTHROPIC_API_KEY and that web search is enabled for your org"
print("OK:", cites[:3])
If the assertion fails with a message that web search is not enabled, an administrator can turn it on in the Claude Console; the tool is enabled for most organizations by default.
FAQ
Covered inline below and mirrored in the page's structured data.
Written by
Ren OkabeFrequently asked questions
How do I add web search to an AI agent in Python?
Install the anthropic SDK, then pass a tool of type web_search_20250305 named web_search in the tools array of client.messages.create. Claude decides when to search, runs the searches server side, and returns a cited answer. You do not implement a search function yourself.
How much does the Claude web search tool cost in 2026?
Web search is $10 per 1,000 searches on the Claude API as of 2026, billed on top of normal input and output token costs. Each search counts as one use regardless of how many results come back, and failed searches are not billed. Use max_uses to cap searches per request.
Does Claude always search the web when the tool is enabled?
No. Claude searches only when a request depends on current, changing, or out of training data information. Stable questions like math or coding concepts are answered directly. You can steer this with your system prompt, and hard cap it with max_uses.
Why do I get a 400 error on the second turn of a web search conversation?
In multi turn conversations you must send the assistant's content blocks back exactly as received, including each search result's encrypted_content field. Rebuilding the message by hand or stripping fields causes a 400 validation error. Append response.content unchanged.
Should I use the built in web_search tool or a search API like Tavily or Brave?
Use the built in web_search tool when you want search, iteration, and citations working immediately for $10 per 1,000 searches. Roll your own client side tool with a search API such as Tavily or Brave when you need a specific index, an existing search contract, or access to private or on prem data the public web tool cannot reach.
Can I limit which websites the agent reads?
Yes. Add allowed_domains to restrict search to a whitelist, or blocked_domains to ban specific sites, on the tool definition. Use one or the other, never both in the same tool, or the API returns a 400 error. You can also localize results with user_location.
Related tutorials
Claude Agent SDK Custom Tools: Give Your Agent Its Own Functions (2026)
A runnable 2026 guide to custom tools in the Claude Agent SDK: define functions with tool() / @tool, bundle them into an in-process MCP server, register with allowedTools, and gate risky calls per argument with canUseTool. TypeScript and Python.
How to Build an AI Agent With the Claude Agent SDK in Python (2026)
A runnable Python quickstart: install the Claude Agent SDK, stream a run with query(), then give the agent your own tools with the @tool decorator and ClaudeSDKClient. Builds a weather agent that chains two tools.
How to Make an AI Agent in 2026 (Step by Step)
The five parts every AI agent has, a runnable Python example in about 40 lines, and an honest guide to the guardrails and framework choices most tutorials skip.
