Claude Tool Use: A Practical Guide to Function Calling

Table of Contents

Claude’s tool use feature lets the model call external functions — databases, APIs, file systems — and incorporate real-time results into responses. Instead of generating static text, Claude can now take actions.

If you’ve already set up the Claude API in Python, this guide will show you how to define tools, handle execution loops, and build a practical database query example.

How Claude Tool Use Actually Works

When you send a message with tools defined, Claude can respond in two ways:

  1. Regular text response — when no tool is needed
  2. Tool use request — Claude returns a tool_use content block asking you to execute a function

Your code executes the function, sends the result back, and Claude generates a final response using that data. This creates a loop:

User Message → Claude (tool_use) → Your Code Executes → Tool Result → Claude (final response)

The key insight: Claude never executes code itself. It requests execution, and you handle the actual function call.

Step 1: Define Your Tool Schema

Tools are defined using JSON Schema. Each tool needs a name, description, and input schema. Here’s a practical example — a customer lookup tool:

import anthropic

client = anthropic.Anthropic()

tools = [
    {
        "name": "lookup_customer",
        "description": "Look up a customer by email address. Returns customer ID, name, plan type, and account status.",
        "input_schema": {
            "type": "object",
            "properties": {
                "email": {
                    "type": "string",
                    "description": "The customer's email address"
                }
            },
            "required": ["email"]
        }
    },
    {
        "name": "get_recent_orders",
        "description": "Get recent orders for a customer. Returns order IDs, dates, and totals.",
        "input_schema": {
            "type": "object",
            "properties": {
                "customer_id": {
                    "type": "integer",
                    "description": "The customer's unique ID"
                },
                "limit": {
                    "type": "integer",
                    "description": "Maximum number of orders to return (default: 5)"
                }
            },
            "required": ["customer_id"]
        }
    }
]

Write descriptions that tell Claude when to use each tool. Be specific about what data gets returned.

Step 2: Implement the Actual Functions

These functions simulate database queries. In production, you’d connect to your actual database:

def lookup_customer(email: str) -> dict:
    """Simulate a database customer lookup."""
    # In production: query your actual database
    customers = {
        "john@example.com": {
            "customer_id": 1042,
            "name": "John Smith",
            "plan": "pro",
            "status": "active",
            "created_at": "2024-03-15"
        },
        "sarah@example.com": {
            "customer_id": 2187,
            "name": "Sarah Connor",
            "plan": "enterprise",
            "status": "active",
            "created_at": "2023-11-02"
        }
    }
    
    if email in customers:
        return customers[email]
    return {"error": f"No customer found with email: {email}"}


def get_recent_orders(customer_id: int, limit: int = 5) -> list:
    """Simulate fetching recent orders."""
    orders = {
        1042: [
            {"order_id": "ORD-5521", "date": "2026-05-08", "total": 149.99},
            {"order_id": "ORD-5380", "date": "2026-04-22", "total": 89.50},
            {"order_id": "ORD-5102", "date": "2026-03-15", "total": 299.00}
        ],
        2187: [
            {"order_id": "ORD-5612", "date": "2026-05-09", "total": 1250.00},
            {"order_id": "ORD-5401", "date": "2026-04-30", "total": 875.00}
        ]
    }
    
    return orders.get(customer_id, [])[:limit]

Step 3: Build the Execution Loop

This is where the real work happens. You need to handle Claude’s tool requests, execute the functions, and feed results back:

def process_tool_call(tool_name: str, tool_input: dict) -> str:
    """Route tool calls to actual functions."""
    if tool_name == "lookup_customer":
        result = lookup_customer(tool_input["email"])
    elif tool_name == "get_recent_orders":
        result = get_recent_orders(
            tool_input["customer_id"],
            tool_input.get("limit", 5)
        )
    else:
        result = {"error": f"Unknown tool: {tool_name}"}
    
    return json.dumps(result)


def chat_with_tools(user_message: str) -> str:
    """Send a message and handle any tool use requests."""
    messages = [{"role": "user", "content": user_message}]
    
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )
        
        # Check if Claude wants to use tools
        if response.stop_reason == "tool_use":
            # Extract tool use blocks
            tool_results = []
            assistant_content = response.content
            
            for block in response.content:
                if block.type == "tool_use":
                    # Execute the requested tool
                    result = process_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result
                    })
            
            # Add assistant response and tool results to conversation
            messages.append({"role": "assistant", "content": assistant_content})
            messages.append({"role": "user", "content": tool_results})
        
        else:
            # No more tool calls — return final text response
            return response.content[0].text

The while True loop handles cases where Claude might chain multiple tool calls. It keeps running until Claude returns a regular text response.

Step 4: Test the Complete System

import json

# Test single tool use
response = chat_with_tools("Look up the customer with email john@example.com")
print(response)

# Test chained tool use
response = chat_with_tools(
    "Find the customer sarah@example.com and show me their recent orders"
)
print(response)

Claude will:

  1. Call lookup_customer to find Sarah
  2. Extract her customer_id from the result
  3. Call get_recent_orders with that ID
  4. Generate a natural language summary

Handling Errors Gracefully

Tool calls can fail. Your functions should return structured errors that Claude can interpret:

def lookup_customer(email: str) -> dict:
    try:
        # Database query here
        result = db.query("SELECT * FROM customers WHERE email = ?", email)
        if not result:
            return {"error": "not_found", "message": f"No customer with email {email}"}
        return result
    except DatabaseError as e:
        return {"error": "database_error", "message": str(e)}

Claude will see the error in the tool result and can respond appropriately — “I couldn’t find a customer with that email address.”

Key Takeaways

  • Tools are schemas, not code — you define what Claude can request, you handle execution
  • Always validate tool inputs — Claude usually follows the schema, but edge cases exist
  • Chain multiple tools when needed — Claude handles multi-step reasoning automatically
  • Return structured errors — let Claude generate user-friendly error messages

This tool use pattern opens up building full AI agents that can interact with real systems — databases, APIs, file systems. Start with simple tools, then compose them into more complex workflows.