Tutorials10 min read

Claude Tool Use: Complete Developer Tutorial (2026)

Learn how to implement Claude tool use (function calling) step by step. Real code examples, parallel tools, error handling, and production patterns.

Claude Tool Use: The Complete Developer Tutorial for 2026

You can ask Claude a question and get a great answer. But if you want Claude to act — search a database, call an API, run a calculation, update a record — you need tool use.

Tool use is the single most important Claude API feature for building real-world applications. It turns Claude from a text generator into an autonomous agent that can interact with your systems. Yet most tutorials gloss over the nuances that make production integrations work reliably.

This tutorial covers everything: how the protocol works, how to define tools correctly, how to handle multi-turn conversations with tool results, parallel tool calls, error handling, and patterns that hold up in production.


What Is Claude Tool Use?

Tool use (also called function calling) lets you give Claude a set of named functions it can invoke. Instead of answering purely from training data, Claude can:

  • Query live databases
  • Call external REST APIs
  • Run calculations or code
  • Read and write files
  • Trigger workflows

The key insight: Claude doesn't execute the tool itself. You define the tool, Claude decides when to call it and with what arguments, and you run the actual function. Claude then incorporates the result into its response.

This creates a clean separation: Claude handles reasoning and decision-making; your code handles execution and side effects.

When to Use Tool Use vs. Prompt Engineering

SituationUse Tool Use?
Claude needs live data (prices, weather, user records)Yes
Claude needs to modify external state (create ticket, send email)Yes
Claude needs to do math with guaranteed precisionYes
Static knowledge from training data is sufficientNo
You can put all relevant data in the context windowNo
The task is purely generative (write, summarize, translate)No

Setting Up Your First Tool

Here's the minimal example — a tool that looks up a product price:

pythonimport anthropic

client = anthropic.Anthropic()

# Step 1: Define the tool schema
tools = [
    {
        "name": "get_product_price",
        "description": "Retrieve the current price of a product by its SKU. Returns price in USD.",
        "input_schema": {
            "type": "object",
            "properties": {
                "sku": {
                    "type": "string",
                    "description": "The product SKU identifier (e.g., 'WIDGET-001')"
                },
                "currency": {
                    "type": "string",
                    "enum": ["USD", "EUR", "GBP"],
                    "description": "The currency for the price. Defaults to USD."
                }
            },
            "required": ["sku"]
        }
    }
]

# Step 2: Send the initial message
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What's the price of WIDGET-001?"}
    ]
)

print(response.stop_reason)   # "tool_use"
print(response.content)       # includes a tool_use block

When Claude decides to call the tool, stop_reason will be "tool_use" and response.content will contain a ToolUseBlock with the tool name and arguments Claude chose.

Anatomy of a Tool Use Response

python# response.content looks like:
[
    TextBlock(text="I'll look that up for you.", type='text'),
    ToolUseBlock(
        id='toolu_01ABC123',
        input={'sku': 'WIDGET-001', 'currency': 'USD'},
        name='get_product_price',
        type='tool_use'
    )
]

Note the id field — you'll need it when you return the result.


Completing the Loop: Returning Tool Results

This is where most tutorials skip critical details. You need to:

  • Extract the tool call from the response
  • Execute your actual function
  • Send the result back as a tool_result message
  • Let Claude generate the final response
  • pythonimport json
    
    def get_product_price(sku: str, currency: str = "USD") -> dict:
        """Your actual business logic here."""
        prices = {
            "WIDGET-001": {"USD": 29.99, "EUR": 27.50, "GBP": 23.99},
            "GADGET-002": {"USD": 49.99, "EUR": 46.00, "GBP": 39.99},
        }
        if sku not in prices:
            return {"error": f"SKU {sku} not found"}
        return {"sku": sku, "price": prices[sku].get(currency, prices[sku]["USD"]), "currency": currency}
    
    
    def run_agent_turn(messages: list, tools: list) -> str:
        """Run one full agent turn, handling tool calls automatically."""
        
        while True:
            response = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=1024,
                tools=tools,
                messages=messages
            )
            
            # If no tool call, we're done — return Claude's text
            if response.stop_reason == "end_turn":
                return next(
                    block.text for block in response.content 
                    if hasattr(block, 'text')
                )
            
            # Process tool calls
            if response.stop_reason == "tool_use":
                # Add Claude's response (including tool_use blocks) to history
                messages.append({"role": "assistant", "content": response.content})
                
                # Process each tool call
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        # Route to the right function
                        if block.name == "get_product_price":
                            result = get_product_price(**block.input)
                        else:
                            result = {"error": f"Unknown tool: {block.name}"}
                        
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,   # Must match the tool_use block id
                            "content": json.dumps(result)
                        })
                
                # Add tool results and loop back
                messages.append({"role": "user", "content": tool_results})
                # Loop continues — Claude will now generate its final response
    
    
    # Run it
    messages = [{"role": "user", "content": "What's the price of WIDGET-001 in EUR?"}]
    answer = run_agent_turn(messages, tools)
    print(answer)
    # "The WIDGET-001 is currently priced at €27.50."


    Parallel Tool Calls

    Claude can call multiple tools simultaneously when the calls are independent. This dramatically speeds up multi-step workflows.

    pythontools = [
        {
            "name": "get_product_price",
            "description": "Get price for a product SKU",
            "input_schema": {
                "type": "object",
                "properties": {"sku": {"type": "string"}},
                "required": ["sku"]
            }
        },
        {
            "name": "get_inventory_count",
            "description": "Get current stock count for a product SKU",
            "input_schema": {
                "type": "object",
                "properties": {"sku": {"type": "string"}},
                "required": ["sku"]
            }
        }
    ]
    
    messages = [{"role": "user", "content": "Check price and stock for WIDGET-001 and GADGET-002"}]

    Claude will likely call all four combinations in parallel — two tools × two SKUs — in a single turn. Your run_agent_turn loop handles this correctly because it processes all tool_use blocks in the response before sending results back.

    Key rule for parallel tool safety: Never let tool calls with side effects (writes, sends, deletes) run in parallel with reads that inform them. If Tool B's behavior depends on Tool A's result, you need sequential execution — handle this in your routing logic or with tool descriptions that guide Claude's sequencing.

    Defining Tools That Claude Uses Correctly

    The quality of your tool definitions directly determines how reliably Claude calls them. Follow these rules:

    1. Write descriptions that explain when to call the tool, not just what it does

    python# Bad — describes what, not when
    "description": "Returns user data from the database"
    
    # Good — tells Claude when to use it
    "description": "Retrieve a user's profile, subscription status, and preferences from the database. Use this whenever you need current user data — do not rely on information the user provides about themselves."

    2. Use specific, constrained types wherever possible

    python# Bad — accepts any string
    "status": {"type": "string"}
    
    # Good — restricts to valid values
    "status": {
        "type": "string",
        "enum": ["active", "inactive", "pending", "cancelled"],
        "description": "Current subscription status"
    }

    3. Make required vs. optional crystal clear

    Mark parameters as required only when the tool genuinely cannot function without them. Optional parameters should have description fields that explain sensible defaults — Claude will infer when to include them.

    4. Use additionalProperties: false for stricter schemas

    python"input_schema": {
        "type": "object",
        "properties": {...},
        "required": ["sku"],
        "additionalProperties": False  # Claude won't hallucinate extra fields
    }


    Error Handling in Production

    Never let a tool execution exception bubble up silently. Return structured errors in the tool_result content — Claude will handle them gracefully and either retry, ask the user for clarification, or explain the issue.

    pythondef safe_tool_call(tool_name: str, tool_input: dict) -> str:
        """Wrapper that catches exceptions and returns structured error results."""
        try:
            if tool_name == "get_product_price":
                result = get_product_price(**tool_input)
            elif tool_name == "get_inventory_count":
                result = get_inventory_count(**tool_input)
            else:
                result = {"error": f"Tool '{tool_name}' is not registered"}
            
            return json.dumps(result)
        
        except ValueError as e:
            return json.dumps({"error": "invalid_input", "message": str(e)})
        except ConnectionError:
            return json.dumps({"error": "service_unavailable", "message": "Database is temporarily unreachable. Try again in a moment."})
        except Exception as e:
            # Log internally, return safe message to Claude
            logger.error(f"Tool {tool_name} failed: {e}", exc_info=True)
            return json.dumps({"error": "internal_error", "message": "An unexpected error occurred."})

    When you return an error, Claude will typically acknowledge it and either:

    • Ask the user to provide missing or corrected information
    • Suggest an alternative approach
    • Report the error clearly in its response

    This is much better than crashing or returning an empty result, which leaves Claude confused.


    Controlling Tool Choice

    By default, Claude decides whether to use tools. You can override this with the tool_choice parameter:

    python# Force Claude to use at least one tool
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        tool_choice={"type": "any"},  # Must call a tool
        messages=messages
    )
    
    # Force a specific tool
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        tool_choice={"type": "tool", "name": "get_product_price"},  # Must call this tool
        messages=messages
    )
    
    # Disable tools entirely for this turn
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        tool_choice={"type": "none"},  # Answer from knowledge only
        messages=messages
    )

    Use tool_choice: "any" when you know the user's request requires live data and you don't want Claude to answer from stale training data. Use tool_choice: "none" at the end of a multi-turn conversation to force a synthesis response once all data has been gathered.


    Production Patterns to Know

    The Agentic Loop with Max Iterations

    Unbounded loops are a production risk. Always cap iterations:

    pythondef run_agent(messages: list, tools: list, max_iterations: int = 10) -> str:
        for i in range(max_iterations):
            response = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=4096,
                tools=tools,
                messages=messages
            )
            
            if response.stop_reason == "end_turn":
                return next(b.text for b in response.content if hasattr(b, 'text'))
            
            if response.stop_reason == "tool_use":
                messages.append({"role": "assistant", "content": response.content})
                results = []
                for block in response.content:
                    if block.type == "tool_use":
                        results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": safe_tool_call(block.name, block.input)
                        })
                messages.append({"role": "user", "content": results})
        
        return "I was unable to complete the task within the allowed number of steps."

    Streaming Tool Use

    For long-running tool calls, stream the response to show progress:

    pythonwith client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        messages=messages
    ) as stream:
        for event in stream:
            if event.type == "content_block_start":
                if hasattr(event.content_block, 'type') and event.content_block.type == "tool_use":
                    print(f"Calling tool: {event.content_block.name}")
            elif event.type == "content_block_delta":
                if hasattr(event.delta, 'text'):
                    print(event.delta.text, end="", flush=True)

    Caching Tool Definitions

    If you use the same large set of tools across many requests, cache the tool definitions with prompt caching to reduce costs:

    pythonresponse = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=tools,
        system=[
            {
                "type": "text",
                "text": "You are a helpful assistant with access to product catalog tools.",
                "cache_control": {"type": "ephemeral"}
            }
        ],
        messages=messages
    )

    Tool definitions are billed as input tokens on every request — for large tool sets (10+ tools with detailed schemas), caching can cut input costs by 60-80%.


    Key Takeaways

    • Tool use is a multi-turn protocol: Claude requests a call → you execute → you return the result → Claude finalizes its response
    • The tool_use_id in your result must exactly match the id in Claude's tool_use block
    • Always return structured JSON in tool results, including for errors — never raise exceptions silently
    • Use additionalProperties: false and enum constraints to prevent Claude from hallucinating invalid inputs
    • Cap your agentic loops with max_iterations to prevent runaway execution
    • Cache tool definitions when using many tools to reduce token costs significantly
    • For parallel tool safety: reads can parallelize freely; writes that depend on read results must sequence


    Next Steps

    Tool use is the foundation of Claude-powered agents. Once you're comfortable with the patterns in this tutorial, the natural progression is:

    • Multi-agent orchestration — coordinate multiple specialized Claude agents, each with their own tool sets
    • MCP servers — expose your tools as a Model Context Protocol server so any Claude client can access them
    • Streaming + real-time UI — show tool execution progress to users instead of a loading spinner

    Ready to go deeper? Explore the CCA certification prep materials on AI for Anything — the Claude Certified Architect exam includes questions on tool use architecture, agent design, and API patterns covered in this guide. Our practice test bank has 200+ questions specifically on Claude's agentic capabilities.

    Ready to Start Practicing?

    300+ scenario-based practice questions covering all 5 CCA domains. Detailed explanations for every answer.

    Free CCA Study Kit

    Get domain cheat sheets, anti-pattern flashcards, and weekly exam tips. No spam, unsubscribe anytime.