What You'll Build
You're going to build a fully functional AI agent in Python that can reason, choose tools, and take autonomous actions — using Anthropic's Claude API. This isn't a chatbot wrapper. It's a real agentic loop where Claude decides what tools to call, processes the results, and keeps going until the job is done.
By the end, you'll have a working multi-tool agent that can search for information, run calculations, and respond intelligently — all without you hardcoding the decision logic. You'll understand every piece of it well enough to swap in your own tools and point it at real business problems.
The complete working agent is built piece by piece in the steps below. Every snippet connects — by Step 4, you'll have one file you can run from your terminal. Copy each block in order, or jump to Step 3 if you just want to see the full agent loop.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one here)
- Basic Python knowledge — functions, classes, dictionaries
anthropicSDK installed:pip install anthropic- A terminal and a text editor (VS Code works great)
Step 1: Set Up Your Claude API Project & Environment
First, let's get the environment ready. Create a new folder for your project and set up a virtual environment so your dependencies stay clean.
terminalmkdir claude-agent cd claude-agent python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install anthropic python-dotenv
Now create a .env file in your project root and add your API key. Never hardcode keys in source files — this habit will save you headaches later.
ANTHROPIC_API_KEY=sk-ant-your-key-here
Create your main Python file. We'll build it up over the next few steps.
agent.pyimport os
import json
import anthropic
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
MODEL = "claude-sonnet-4-6"
claude-sonnet-4-6 throughout this tutorial. It has strong tool-use reasoning and is cost-efficient for agentic workloads. Don't swap this for a different model mid-tutorial — tool calling behavior can vary.
Step 2: Define Your Agent's Tools & Capabilities
Tools are how your agent interacts with the world. You define them as JSON schemas, and Claude decides when and how to call them. Think of this as handing your agent a toolbox and letting it pick the right wrench.
We're going to define three tools: a web search simulator, a calculator, and a weather lookup. In production you'd wire these to real APIs — but these stubs let you see exactly how the tool-calling flow works without any external dependencies.
agent.py (add below your imports)# Tool schemas — Claude reads these to understand what each tool does
# and what arguments it needs to call them correctly
TOOLS = [
{
"name": "search_web",
"description": "Search the web for current information on a topic. Use this when you need facts, news, or data you don't already know.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up"
}
},
"required": ["query"]
}
},
{
"name": "calculate",
"description": "Perform mathematical calculations. Use this for any arithmetic, percentages, or numeric operations.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A valid Python math expression, e.g. '(150 * 0.08) + 150'"
}
},
"required": ["expression"]
}
},
{
"name": "get_weather",
"description": "Get the current weather for a specific city.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g. 'Naples, FL'"
}
},
"required": ["city"]
}
}
]
Now write the actual Python functions those tools map to. This is where you'd plug in real APIs — but for now, these return realistic mock data.
agent.py (add below TOOLS)def search_web(query: str) -> dict:
"""Simulates a web search. Replace with real search API in production."""
mock_results = {
"real estate naples fl": "Naples, FL median home price is $625,000 as of Q2 2026. Inventory is up 12% year-over-year.",
"anthropic claude api": "Anthropic's Claude API supports tool use, vision, and long context up to 200k tokens.",
"default": f"Search results for '{query}': Found 10 relevant articles on the topic."
}
# Return matching result or the default
for key in mock_results:
if key in query.lower():
return {"results": mock_results[key], "query": query}
return {"results": mock_results["default"], "query": query}
def calculate(expression: str) -> dict:
"""Safely evaluates a math expression using only numeric operations."""
try:
# Restrict eval to safe math operations only
allowed_names = {"__builtins__": {}}
result = eval(expression, allowed_names, {}) # noqa: S307
return {"result": result, "expression": expression}
except Exception as e:
return {"error": str(e), "expression": expression}
def get_weather(city: str) -> dict:
"""Simulates a weather API call. Replace with OpenWeatherMap or similar."""
mock_weather = {
"naples": {"temp_f": 88, "condition": "Partly Cloudy", "humidity": 74},
"miami": {"temp_f": 91, "condition": "Sunny", "humidity": 80},
"default": {"temp_f": 75, "condition": "Clear", "humidity": 60}
}
city_lower = city.lower().split(",")[0].strip()
weather = mock_weather.get(city_lower, mock_weather["default"])
return {"city": city, **weather}
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""Routes a tool call from Claude to the correct Python function."""
if tool_name == "search_web":
result = search_web(**tool_input)
elif tool_name == "calculate":
result = calculate(**tool_input)
elif tool_name == "get_weather":
result = get_weather(**tool_input)
else:
result = {"error": f"Unknown tool: {tool_name}"}
return json.dumps(result)
Step 3: Implement the Agent Loop with Function Calling
This is the heart of the tutorial. The agent loop is what makes this system "agentic" — Claude responds, you check if it wants to use a tool, you run that tool, feed the result back, and repeat until Claude gives you a final answer.
Most tutorials skip explaining why you need a loop. Here's the reason: Claude might need to call three tools in sequence before it has enough information to answer. The loop handles all of that automatically.
agent.py (add below execute_tool)class ClaudeAgent:
"""
A simple autonomous agent built on Claude's tool-use API.
Runs an agentic loop until Claude returns a final text response.
"""
def __init__(self, system_prompt: str = None, max_iterations: int = 10):
self.client = client
self.model = MODEL
self.tools = TOOLS
self.max_iterations = max_iterations
self.system_prompt = system_prompt or (
"You are a helpful AI assistant with access to tools. "
"Use tools whenever they would improve your answer. "
"Always explain what you're doing and why."
)
def run(self, user_message: str) -> str:
"""
Run the agent on a user message.
Returns the final text response from Claude.
"""
print(f"\n{'='*60}")
print(f"USER: {user_message}")
print(f"{'='*60}")
# Start with the user's message
messages = [{"role": "user", "content": user_message}]
iteration = 0
while iteration < self.max_iterations:
iteration += 1
print(f"\n[Iteration {iteration}] Calling Claude...")
# Send messages to Claude with the tool definitions
response = self.client.messages.create(
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=self.tools,
messages=messages
)
print(f"[Stop reason: {response.stop_reason}]")
# If Claude is done, extract and return the final text
if response.stop_reason == "end_turn":
final_text = self._extract_text(response)
print(f"\nAGENT: {final_text}")
return final_text
# If Claude wants to use a tool, handle it
if response.stop_reason == "tool_use":
# Add Claude's response (including tool_use blocks) to messages
messages.append({"role": "assistant", "content": response.content})
# Process every tool call in this response
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"[Tool call: {block.name}({block.input})]")
result = execute_tool(block.name, block.input)
print(f"[Tool result: {result}]")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Feed all tool results back to Claude in a single user turn
messages.append({"role": "user", "content": tool_results})
continue
# Unexpected stop reason — break to avoid infinite loop
print(f"[Unexpected stop reason: {response.stop_reason}]")
break
return "Agent reached maximum iterations without completing the task."
def _extract_text(self, response) -> str:
"""Pull the final text out of Claude's response content blocks."""
text_parts = []
for block in response.content:
if hasattr(block, "text"):
text_parts.append(block.text)
return " ".join(text_parts).strip()
Now add the entry point so you can run this from your terminal and see it in action.
agent.py (add at the bottom)if __name__ == "__main__":
agent = ClaudeAgent()
# Example 1: Multi-step reasoning with tools
response1 = agent.run(
"What's the weather like in Naples, FL? Also, if a home there costs $625,000 "
"and the property tax rate is 1.1%, what would the annual property tax be?"
)
print("\n" + "="*60)
# Example 2: Research question
response2 = agent.run(
"Search for information about the Claude API and summarize what you find."
)
Run it with python agent.py and you should see output like this:
============================================================
USER: What's the weather like in Naples, FL? Also, if a home there costs $625,000
and the property tax rate is 1.1%, what would the annual property tax be?
============================================================
[Iteration 1] Calling Claude...
[Stop reason: tool_use]
[Tool call: get_weather({'city': 'Naples, FL'})]
[Tool result: {"city": "Naples, FL", "temp_f": 88, "condition": "Partly Cloudy", "humidity": 74}]
[Tool call: calculate({'expression': '625000 * 0.011'})]
[Tool result: {"result": 6875.0, "expression": "625000 * 0.011"}]
[Iteration 2] Calling Claude...
[Stop reason: end_turn]
AGENT: Right now in Naples, FL it's 88°F and partly cloudy with 74% humidity —
classic Southwest Florida summer weather. As for the property taxes on a $625,000
home at a 1.1% rate, you'd be looking at $6,875 per year, or roughly $573 per month
added to your mortgage escrow.
============================================================
USER: Search for information about the Claude API and summarize what you find.
============================================================
[Iteration 1] Calling Claude...
[Stop reason: tool_use]
[Tool call: search_web({'query': 'anthropic claude api'})]
[Tool result: {"results": "Anthropic's Claude API supports tool use, vision, and long context up to 200k tokens.", "query": "anthropic claude api"}]
[Iteration 2] Calling Claude...
[Stop reason: end_turn]
AGENT: Based on what I found, the Claude API from Anthropic is a powerful platform
that supports tool use (like what I'm doing right now), vision capabilities for
analyzing images, and a long context window of up to 200,000 tokens — meaning it
can process very large documents in a single call.
Step 4: Add Error Handling & Retry Logic
Real agents hit real problems — network timeouts, API rate limits, malformed tool inputs. Without error handling, one bad API call crashes the whole thing. This wrapper catches those issues and retries with exponential backoff.
agent.py (replace your client.messages.create call inside run())import time
def call_claude_with_retry(
client: anthropic.Anthropic,
max_retries: int = 3,
**kwargs
):
"""
Calls the Claude API with automatic retry on rate limits and server errors.
Uses exponential backoff: waits 2s, then 4s, then 8s between retries.
"""
for attempt in range(max_retries):
try:
return client.messages.create(**kwargs)
except anthropic.RateLimitError:
wait_time = 2 ** (attempt + 1) # 2, 4, 8 seconds
print(f"[Rate limited. Retrying in {wait_time}s...]")
time.sleep(wait_time)
except anthropic.APIStatusError as e:
# Retry on server errors (5xx), raise immediately on client errors (4xx)
if e.status_code >= 500:
wait_time = 2 ** (attempt + 1)
print(f"[Server error {e.status_code}. Retrying in {wait_time}s...]")
time.sleep(wait_time)
else:
raise # 4xx errors are your bug, not a transient issue
except anthropic.APIConnectionError:
wait_time = 2 ** (attempt + 1)
print(f"[Connection error. Retrying in {wait_time}s...]")
time.sleep(wait_time)
raise RuntimeError(f"Claude API call failed after {max_retries} attempts.")
Now update the run method inside your ClaudeAgent class to use this function. Replace the response = self.client.messages.create(...) line with:
response = call_claude_with_retry(
self.client,
model=self.model,
max_tokens=4096,
system=self.system_prompt,
tools=self.tools,
messages=messages
)
How It Works
Here's what's actually happening under the hood when you call agent.run().
You send Claude a message along with your tool definitions. Claude reads the tool schemas and decides whether it needs to call any of them. If it does, the API returns a response with stop_reason: "tool_use" instead of stop_reason: "end_turn".
Your loop catches that, extracts the tool_use blocks, runs the actual Python functions, and sends the results back to Claude as tool_result blocks. Claude processes those results and either calls more tools or writes its final answer. The whole conversation — including tool calls and results — stays in the messages list so Claude always has full context.
The max_iterations cap is your safety net. Without it, a confused agent could loop forever. Ten iterations is plenty for most tasks — complex research agents might need 20 or 30.
Common Errors and Fixes
Error 1: AuthenticationError on startup
anthropic.AuthenticationError: 401 {"type":"error","error":{"type":"authentication_error","message":"invalid x-api-key"}}
Fix: Your API key isn't loading. Double-check your .env file is in the same directory as agent.py, that you called load_dotenv() before creating the client, and that the key starts with sk-ant-. Also confirm you're not accidentally passing the literal string "your-key-here".
Error 2: Tool result format causes a 400
anthropic.BadRequestError: 400 {"type":"error","error":{"type":"invalid_request_error","message":"tool_result content must be a string or array"}}
Fix: Your execute_tool function must return a string, not a dict. That's why we use json.dumps(result) at the end of execute_tool. If you return a raw Python dictionary, the API will reject it with this exact error.
Error 3: Agent loops forever without calling tools
[Iteration 1] Calling Claude... [Stop reason: end_turn] AGENT: I don't have access to real-time information to answer that question.
Fix: Claude isn't using your tools because the tool descriptions are too vague or the system prompt doesn't encourage tool use. Be explicit in your tool's description field about when Claude should reach for it. Update your system prompt to say something like: "Always use available tools when they would help you give a more accurate answer."
Next Steps
You've got a working agent. Here's where to take it next:
- Connect real tools: Replace the mock functions with actual API calls — Serper or Brave Search for web search, OpenWeatherMap for weather, or a database query for business data.
- Add memory: Persist the
messageslist to a file or database so your agent remembers previous conversations across sessions. - Build a multi-agent system: Have one orchestrator agent delegate tasks to specialized sub-agents — one for research, one for writing, one for data analysis.
- Expose it as an API: Wrap your
ClaudeAgentclass in a FastAPI endpoint so any frontend or automation tool can call it over HTTP.
Frequently Asked Questions
How does Claude API tool use work?
You pass Claude a list of tool definitions (JSON schemas) alongside your messages. When Claude decides a tool would help, it returns a tool_use block with the tool name and input arguments instead of just text. Your code runs the actual function, returns