Programming &
Frameworks
This is where the concepts become code. You'll learn the Python patterns that appear in every agent codebase, how the Anthropic SDK's tool use loop actually works at the API level, and when to reach for a framework like LangGraph versus building on the raw SDK. The lab ends with a working tool-calling agent you wrote yourself.
Why Python, and What You Actually Need
Python is the dominant language for agent development — not because it's the fastest at runtime, but because virtually every AI SDK, ML library, and vector database client ships a Python SDK first. For building agents, you don't need deep Python expertise, but you do need fluency with a specific set of patterns that appear constantly.
python-dotenv library loads a .env file into os.environ at startup. All major SDKs read keys from environment variables by default.python -m venv .venv (or uv) to isolate project dependencies. Agent projects pull in many libraries — isolating them prevents version conflicts across projects.Minimal project setup
# Create and activate a virtual environment python -m venv .venv source .venv/bin/activate # Linux / macOS # .venv\Scripts\activate # Windows # Install the core agent stack pip install anthropic python-dotenv pydantic # Create your .env file (never commit this to git) echo "ANTHROPIC_API_KEY=your_key_here" > .env echo ".env" >> .gitignore
How Every API Call Is Structured
The Anthropic SDK's messages.create is the primitive every agent is built on. Before adding any framework, you need to understand this call deeply — what goes in, what comes out, and how the conversation state is maintained between turns.
{"role": "user"|"assistant", "content": ...} dicts. You are responsible for maintaining this list across turns — the API is stateless.
import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env response = client.messages.create( model="claude-opus-4-6", # check docs.anthropic.com for current models max_tokens=1024, system="You are a helpful assistant.", messages=[ {"role": "user", "content": "What is the capital of France?"} ] ) print(response.content[0].text) # "Paris is the capital of France." print(response.stop_reason) # "end_turn" print(response.usage) # input_tokens, output_tokens
messages.create is independent. The model has no memory of previous calls. You must pass the full conversation history in the messages list on every call. This is why agents maintain a messages list and append to it after each turn.
How Tool Calling Works at the API Level
Tool use is the mechanism that turns a single-response LLM call into an agent loop. When you pass tool definitions to the API, the model may respond with a tool_use content block instead of (or alongside) text — indicating it wants to call one of your tools. You execute the tool and send the result back. This is the complete agent loop.
messages.create(messages=[...], tools=[calculator_tool])
content: [TextBlock("I'll calculate that."), ToolUseBlock(id="tu_01", name="calculator", input={"op":"multiply","a":1234,"b":5678})]
result = run_calculator("multiply", 1234, 5678) → 7006652
messages.append({"role":"user", "content":[{"type":"tool_result","tool_use_id":"tu_01","content":"7006652"}]})
content: [TextBlock("1234 × 5678 = 7,006,652")]
Defining a tool — the JSON Schema format
Every tool definition has three required fields. The input_schema is standard JSON Schema — the model uses it to know what parameters to pass.
calculator_tool = {
"name": "calculator",
"description": "Performs basic arithmetic. Use this whenever the user
asks for a calculation involving numbers.",
"input_schema": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["add", "subtract", "multiply", "divide"],
"description": "The arithmetic operation to perform"
},
"a": {"type": "number", "description": "First operand"},
"b": {"type": "number", "description": "Second operand"}
},
"required": ["operation", "a", "b"]
}
}
LangChain, LangGraph, CrewAI, AutoGen
Several open-source frameworks have emerged to handle the plumbing that appears in every agent project: managing conversation state, registering tool libraries, implementing retry logic, connecting to vector stores, and providing observability. Understanding what each one does — and what it costs in complexity — lets you make an informed choice.
| Framework | Best for | Key strength | Watch out for |
|---|---|---|---|
| LangChain | Rapid prototyping, broad tool ecosystem | Huge library of integrations | Abstractions hide API details; debugging is harder |
| LangGraph | Production agents needing state, cycles, human checkpoints | Explicit state machine; resumable workflows | More setup than simple chains |
| CrewAI | Role-based multi-agent pipelines with minimal code | Fast to define agent crews and task sequences | Less control over routing logic |
| AutoGen | Research, conversational multi-agent experiments | Flexible agent-to-agent communication protocols | Less battle-tested in production than LangGraph |
| Raw SDK | Learning, simple agents, maximum control | You see exactly what happens — no magic | You write more boilerplate yourself |
When to Use Each
Anthropic's own documentation is explicit on this point: for simple agents, adding a framework is often unnecessary overhead. The right choice depends on what complexity you're actually dealing with.
Verified References
| Source | Type | Covers | Recency |
|---|---|---|---|
| Anthropic — Messages API Reference | Official docs | messages.create parameters, response structure, stop reasons | Maintained 2024–2026 |
| Anthropic — Tool Use Documentation | Official docs | Tool definition schema, tool_use loop, tool_result format | Maintained 2024–2026 |
| Anthropic Python SDK (GitHub) | Official SDK | Client setup, async client, code examples | Maintained 2023–2026 |
| LangGraph Documentation | Official docs | Graph-based agent architecture, state persistence, human-in-the-loop | Maintained 2024–2026 |
| LangChain Documentation | Official docs | Framework overview, chains, tool integrations ecosystem | Maintained 2022–2026 |
Build Your First Tool-Using Agent
You'll implement the full tool use loop from scratch using the Anthropic SDK — no framework. By the end you'll have a working agent that can call a calculator tool, observe the result, and deliver a final answer. This is the same pattern used by every agent framework under the hood.
Create a new directory, set up a virtual environment, and install the SDK.
mkdir agent-lab && cd agent-lab python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install anthropic python-dotenv # Create .env with your API key echo "ANTHROPIC_API_KEY=your_key_here" > .env
Get your API key from console.anthropic.com → API Keys.
Create agent.py and define a calculator tool. Note the JSON Schema structure — this is what the model uses to know what inputs to pass.
import os import json import anthropic from dotenv import load_dotenv load_dotenv() client = anthropic.Anthropic() # Tool definition — JSON Schema for inputs TOOLS = [ { "name": "calculator", "description": ( "Performs arithmetic: add, subtract, multiply, divide. " "Use this whenever the user asks for a calculation." ), "input_schema": { "type": "object", "properties": { "operation": { "type": "string", "enum": ["add", "subtract", "multiply", "divide"], "description": "The arithmetic operation" }, "a": {"type": "number", "description": "First operand"}, "b": {"type": "number", "description": "Second operand"} }, "required": ["operation", "a", "b"] } } ]
Write the Python function that actually runs when the model requests the calculator tool. This runs locally — the model never executes code directly.
def run_calculator(operation: str, a: float, b: float) -> str: """Execute the calculator tool and return result as string.""" if operation == "add": result = a + b elif operation == "subtract": result = a - b elif operation == "multiply": result = a * b elif operation == "divide": if b == 0: return "Error: division by zero" result = a / b else: return f"Unknown operation: {operation}" return str(result) def execute_tool(tool_name: str, tool_input: dict) -> str: """Route tool calls to the correct executor function.""" if tool_name == "calculator": return run_calculator( tool_input["operation"], tool_input["a"], tool_input["b"] ) raise ValueError(f"Unknown tool: {tool_name}")
Write the core loop. It calls the API, checks the stop reason, executes tools if requested, sends results back, and repeats until the model signals end_turn.
def run_agent(user_message: str) -> str: """Run the agent loop until end_turn or max iterations.""" messages = [ {"role": "user", "content": user_message} ] max_iterations = 10 # guard against infinite loops for iteration in range(max_iterations): print(f"\n[iteration {iteration + 1}] calling API...") response = client.messages.create( model="claude-opus-4-6", # use a current model from docs.anthropic.com max_tokens=1024, tools=TOOLS, messages=messages ) # Append the assistant's response to history messages.append({ "role": "assistant", "content": response.content }) print(f" stop_reason: {response.stop_reason}") # Done — extract and return the final text if response.stop_reason == "end_turn": for block in response.content: if hasattr(block, "text"): return block.text return "(no text response)" # Model wants to call tools — execute each one if response.stop_reason == "tool_use": 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 }) # Send tool results back as a user message messages.append({ "role": "user", "content": tool_results }) return "Max iterations reached without end_turn." if __name__ == "__main__": answer = run_agent("What is 1234 multiplied by 5678?") print(f"\nAgent: {answer}")
Run the agent and observe the loop in action. You should see the iteration count, the tool call with its inputs, the tool result, and the final answer.
python agent.py
[iteration 1] calling API...
stop_reason: tool_use
tool call: calculator({'operation': 'multiply', 'a': 1234, 'b': 5678})
tool result: 7006652.0
[iteration 2] calling API...
stop_reason: end_turn
Agent: 1234 multiplied by 5678 equals 7,006,652.
Notice the two iterations: first the model decides to call the tool (stop_reason: tool_use), then after receiving the result it produces the final answer (stop_reason: end_turn). This is the complete ReAct loop from Section 01 — in code.
Add a get_today tool that returns the current date, then ask the agent a question that requires both tools: "If today's date number is multiplied by 100, what do you get?" This forces a multi-tool, multi-iteration loop.
{
"name": "get_today",
"description": "Returns today's date as a string (YYYY-MM-DD).",
"input_schema": {
"type": "object",
"properties": {},
"required": []
}
}
from datetime import date if tool_name == "get_today": return str(date.today())
Observe how the agent now runs three or more iterations — calling get_today first, then calculator with the extracted date number, then returning the final answer. You've built a multi-tool, multi-step agent.