Course  /  03 · Programming & Frameworks
SECTION 03 CORE LAB

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.

01 · THE PYTHON STACK FOR AGENT BUILDERS

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.

🔄
async / await
Most agent loops make many I/O calls — API requests, tool executions, database reads. Async Python lets these run concurrently instead of sequentially. Most modern AI SDKs offer async clients.
ESSENTIAL PATTERN
📐
Pydantic models
Pydantic provides runtime type validation for Python. Agent tool inputs and outputs are JSON — Pydantic models validate that structure and catch malformed tool calls before they cause silent failures.
ESSENTIAL PATTERN
🔑
Environment variables
API keys should never be hardcoded. The python-dotenv library loads a .env file into os.environ at startup. All major SDKs read keys from environment variables by default.
ESSENTIAL PATTERN
📦
Virtual environments
Use python -m venv .venv (or uv) to isolate project dependencies. Agent projects pull in many libraries — isolating them prevents version conflicts across projects.
ESSENTIAL PATTERN

Minimal project setup

BASH
# 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
02 · THE ANTHROPIC SDK — MESSAGES API

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.

// ANATOMY OF A messages.create CALL
model Which model to use. Pass the model ID string. Check Anthropic docs for current available models.
max_tokens Maximum tokens the model may generate in this response. Required. Does not include input tokens — you pay for both.
system The system prompt — the agent's persistent instructions, persona, and constraints. Separate from the messages list. Set it once per agent session.
messages The conversation history as a list of {"role": "user"|"assistant", "content": ...} dicts. You are responsible for maintaining this list across turns — the API is stateless.
tools List of tool definitions the model may call. Each tool has a name, description, and JSON Schema for its input. Optional — omit for non-agent prompts.
PYTHON
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
The API is stateless. Each call to 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.
03 · THE TOOL USE LOOP

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.

// THE TOOL USE LOOP — FULL SEQUENCE
YOU → API
messages.create(messages=[...], tools=[calculator_tool])
API → YOU   stop_reason = "tool_use"
content: [TextBlock("I'll calculate that."), ToolUseBlock(id="tu_01", name="calculator", input={"op":"multiply","a":1234,"b":5678})]
YOU — execute the tool locally
result = run_calculator("multiply", 1234, 5678) → 7006652
YOU → API   send tool result back
messages.append({"role":"user", "content":[{"type":"tool_result","tool_use_id":"tu_01","content":"7006652"}]})
API → YOU   stop_reason = "end_turn"
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.

PYTHON
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"]
    }
}
Description quality matters. The model decides when to call a tool based on its description. Be specific about what the tool does and when to use it. Vague descriptions lead to the model either over-calling or under-calling the tool.
04 · THE FRAMEWORK LANDSCAPE

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.

LANGCHAIN
The Component Library
The original Python framework for LLM applications. Provides chains, tool integrations, document loaders, and a large ecosystem of connectors. Broad but has a reputation for abstractions that obscure what's happening underneath.
WIDELY USED (2022–2026)
LANGGRAPH
Graph-Based State Machines
Built by the LangChain team. Models agent workflows as graphs where nodes are processing steps and edges define routing logic. Handles state persistence, cycles, and human-in-the-loop interrupts natively. Strong fit for supervised agents.
WIDELY USED (2024–2026)
CREWAI
Role-Based Multi-Agent
Organizes agents as a "crew" of role-playing specialists coordinated by a process (sequential or hierarchical). Minimal boilerplate for multi-agent pipelines. Less flexible than LangGraph for complex routing.
EMERGING (2024–2026)
AUTOGEN
Conversational Multi-Agent
Microsoft Research's framework. Agents communicate through a message-passing protocol, supporting both LLM-based and human-in-the-loop agents in the same conversation. Strong research lineage.
EMERGING (2024–2026)
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
📎 Sources: LangChain Docs  ·  LangGraph Docs  ·  CrewAI Docs  ·  AutoGen Docs
05 · FRAMEWORK vs RAW SDK

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.

// DECISION GUIDE — FRAMEWORK OR RAW SDK?
USE RAW SDK WHEN
✓ You are learning — you need to see every step to understand what's happening
✓ Your agent has one or two tools and a simple loop
✓ You need maximum debuggability and control
✓ You are building a custom architecture that doesn't fit standard patterns
REACH FOR A FRAMEWORK WHEN
✓ You need state persistence across runs (LangGraph checkpointing)
✓ You need human-in-the-loop interrupts — pause, approve, resume (LangGraph)
✓ You need to connect to many external systems quickly (LangChain integrations)
✓ You are building multi-agent pipelines with role specialization (CrewAI, AutoGen)
✓ You need observability — tracing, replay, debugging agent runs (LangSmith + LangGraph)
Course approach: This section's lab uses the raw Anthropic SDK so you can see every step clearly. Later sections (07 Planning, 14 Multi-Agent) introduce LangGraph where the graph-based state management genuinely earns its complexity.
SOURCES USED IN THIS SECTION

Verified References

SourceTypeCoversRecency
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
HANDS-ON LAB

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.

🔬
Tool-Using Agent — Raw Anthropic SDK
PYTHON · ~50 LINES · ANTHROPIC API KEY REQUIRED
1
Set up your environment

Create a new directory, set up a virtual environment, and install the SDK.

BASH
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.

2
Define the tool

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.

PYTHON — agent.py
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"]
        }
    }
]
3
Implement the tool executor

Write the Python function that actually runs when the model requests the calculator tool. This runs locally — the model never executes code directly.

PYTHON — agent.py (continued)
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}")
4
Implement the agent loop

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.

PYTHON — agent.py (continued)
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}")
5
Run it and read the output

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.

BASH
python agent.py
EXPECTED OUTPUT
[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.

6
Extend: add a second tool

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.

PYTHON — add to TOOLS list
{
    "name": "get_today",
    "description": "Returns today's date as a string (YYYY-MM-DD).",
    "input_schema": {
        "type": "object",
        "properties": {},
        "required": []
    }
}
PYTHON — add to execute_tool()
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.

Finished the theory and completed the lab? Mark this section complete.

Last updated: