Claude API Tool Use Tutorial 2026: Function Calling for Developers
ai-productivity

Claude API Tool Use Tutorial 2026: Function Calling for Developers

Ricardo Gil
September 2, 2026
10 min read
#claude-api #tool-use #python #ai-agents #developer-productivity
πŸ›’

Products in This Post

Affiliate links

As an Amazon Associate I earn from qualifying purchases at no extra cost to you.

Tool use β€” also called function calling β€” is how you give Claude the ability to interact with the real world. Instead of a static Q&A exchange, you define a set of β€œtools” (functions your code can run), describe them to Claude in JSON Schema format, and let Claude decide when to call them. Claude returns a structured call with the right arguments; your code executes the function and sends the result back. The loop continues until Claude has what it needs to give a final answer.

This is the foundation of every serious AI agent. Without tool use, Claude is a text transformer. With it, Claude becomes an orchestrator that can query databases, call APIs, read files, search the web, or control your infrastructure. This tutorial uses the official anthropic Python SDK and covers everything from a single tool call to a multi-step agentic loop you’d ship in production.

How Tool Use Works at the API Level

The flow is straightforward once you see it:

  1. You send Claude a list of tool definitions (name, description, input schema) alongside the user’s message.
  2. If Claude decides a tool is needed, it returns a tool_use content block instead of plain text.
  3. Your code executes the function and sends back a tool_result message.
  4. Claude reads the result and either calls another tool or produces its final answer.

One critical thing to internalize: Claude never executes code. It only produces structured output describing what it wants to call and with what arguments. Your code is the executor. This separation keeps you in full control of every side effect.

Environment Setup

Install the SDK and set your API key:

bash
pip install anthropic python-dotenv
bash
# .env
ANTHROPIC_API_KEY=sk-ant-...
python
import anthropic
import json
import os
from dotenv import load_dotenv

load_dotenv()
client = anthropic.Anthropic()

Your First Tool: A Currency Converter

Start concrete. We’ll give Claude a currency conversion tool, then ask it a question that requires using it.

python
tools = [
    {
        "name": "convert_currency",
        "description": "Convert an amount from one currency to another using current exchange rates.",
        "input_schema": {
            "type": "object",
            "properties": {
                "amount": {"type": "number", "description": "The amount to convert"},
                "from_currency": {"type": "string", "description": "Source currency code, e.g. USD, EUR, GBP"},
                "to_currency": {"type": "string", "description": "Target currency code"}
            },
            "required": ["amount", "from_currency", "to_currency"]
        }
    }
]

def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
    rates = {"USD": 1.0, "EUR": 0.92, "GBP": 0.79, "JPY": 149.5}
    usd_amount = amount / rates.get(from_currency, 1)
    converted = usd_amount * rates.get(to_currency, 1)
    return {"result": round(converted, 2), "from": from_currency, "to": to_currency}

response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "How much is 500 EUR in USD and GBP?"}]
)

print(f"Stop reason: {response.stop_reason}")  # "tool_use"

When stop_reason is "tool_use", Claude wants to call your function. Extract the block, execute it, and send the result back:

python
tool_results = []

for block in response.content:
    if block.type == "tool_use":
        if block.name == "convert_currency":
            result = convert_currency(**block.input)
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": json.dumps(result)
        })

final_response = client.messages.create(
    model="claude-sonnet-4-5",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "How much is 500 EUR in USD and GBP?"},
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": tool_results}
    ]
)

print(final_response.content[0].text)

Building a Proper Agentic Loop

The two-call example above handles one tool. Real agents chain multiple calls β€” Claude reads a file, finds a reference, fetches another file, writes a summary. Here’s the reusable loop pattern you should reach for:

python
def run_agent(user_message: str, tools: list, dispatcher, max_iterations: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]
    iteration = 0

    while iteration < max_iterations:
        iteration += 1

        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )

        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text

        if response.stop_reason == "tool_use":
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = dispatcher(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": json.dumps(result)
                    })
            messages.append({"role": "user", "content": tool_results})

    return "Max iterations reached."

The max_iterations guard is non-optional in production. An unbounded loop is a real cost and latency risk. Cap it, log when it fires, and tune the limit based on what your agent actually needs.

Real-World Example: A File System Research Agent

Here’s something you’d actually deploy: an agent that lists files, reads them, and writes a summary to disk. This is the core of any document processing pipeline.

python
FS_TOOLS = [
    {
        "name": "list_files",
        "description": "List files in a directory. Returns names and sizes.",
        "input_schema": {
            "type": "object",
            "properties": {"directory": {"type": "string"}},
            "required": ["directory"]
        }
    },
    {
        "name": "read_file",
        "description": "Read the full text content of a file.",
        "input_schema": {
            "type": "object",
            "properties": {"path": {"type": "string"}},
            "required": ["path"]
        }
    },
    {
        "name": "write_file",
        "description": "Write content to a file, overwriting if it exists.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string"},
                "content": {"type": "string"}
            },
            "required": ["path", "content"]
        }
    }
]

def fs_dispatcher(name: str, inputs: dict) -> dict:
    try:
        if name == "list_files":
            entries = os.listdir(inputs["directory"])
            files = [
                {"name": e, "size": os.path.getsize(os.path.join(inputs["directory"], e))}
                for e in entries
                if os.path.isfile(os.path.join(inputs["directory"], e))
            ]
            return {"files": files}
        elif name == "read_file":
            with open(inputs["path"], "r") as f:
                return {"content": f.read()}
        elif name == "write_file":
            with open(inputs["path"], "w") as f:
                f.write(inputs["content"])
            return {"success": True, "path": inputs["path"]}
    except Exception as ex:
        return {"error": str(ex)}
    return {"error": f"Unknown tool: {name}"}

# Run it
result = run_agent(
    "List the markdown files in ./docs, read each one, and write a combined summary to ./docs/SUMMARY.md",
    FS_TOOLS,
    fs_dispatcher
)
print(result)

On a docs directory with five files, Claude will call list_files once, read_file five times, then write_file once β€” all autonomously, with correct paths inferred from the directory listing. No orchestration glue on your side beyond the loop.

Parallel Tool Calls

Claude 3.5+ can emit multiple tool calls in a single assistant message when it recognizes they’re independent. Instead of reading three files sequentially (three round trips), Claude batches them. Execute them concurrently to cut latency proportionally:

python
import asyncio

async def dispatch_parallel(tool_uses: list, dispatcher) -> list:
    async def run_one(tu):
        result = await asyncio.to_thread(dispatcher, tu.name, tu.input)
        return {
            "type": "tool_result",
            "tool_use_id": tu.id,
            "content": json.dumps(result)
        }
    return await asyncio.gather(*[run_one(tu) for tu in tool_uses])

Claude will parallelize when it recognizes calls are independent β€” you just need your dispatcher to support concurrent execution. For I/O-bound tools like API calls or database queries, this is a straightforward win.

Streaming Tool Use

For production UIs where you want to show live progress, use the streaming API. The SDK accumulates tool use blocks for you automatically:

python
with client.messages.stream(
    model="claude-sonnet-4-5",
    max_tokens=2048,
    tools=FS_TOOLS,
    messages=messages
) as stream:
    for event in stream:
        if event.type == "content_block_start":
            if event.content_block.type == "tool_use":
                print(f"\n-> Calling: {event.content_block.name}", flush=True)
        elif event.type == "text":
            print(event.text, end="", flush=True)

    final = stream.get_final_message()  # Fully assembled tool_use blocks here

Error Handling: When Tools Fail

Tools fail. Files don’t exist, APIs time out, queries return empty results. Tell Claude explicitly when something goes wrong:

python
tool_results.append({
    "type": "tool_result",
    "tool_use_id": block.id,
    "content": json.dumps({"error": "File not found: /docs/missing.md"}),
    "is_error": True
})

Claude adapts: it might try a different path, ask for clarification, or explain why the task can’t complete. You don’t need explicit retry logic at the orchestration layer β€” Claude handles it conversationally. What you do need is structured error output so Claude gets actionable information, not a raw Python traceback.

Hardware for Running the Tool Dispatch Layer

If your agent tools call locally hosted services β€” a self-hosted Postgres, a private n8n instance, a local vector database β€” you want a machine running 24/7 with minimal power draw. The Beelink EQ12 Mini PC (~$189) is exactly right for this: always-on, fanless at idle, and fast enough for I/O-bound agent workloads. Pair it with 32GB DDR4 RAM (~$59) and you can also run a small Ollama model alongside it for offline fallback inference when the Claude API is unavailable.

For agents that persist large knowledge bases between sessions β€” a research agent accumulating retrieved documents or a local vector store β€” fast random read/write matters. A WD Black 2TB NVMe SSD (~$129) keeps SQLite and ChromaDB queries snappy even as your corpus grows into the tens of thousands of entries.

Best Practices for Production Tool Use

Write Tool Descriptions Like API Documentation

Claude uses your descriptions to decide when and how to call each tool. Vague descriptions produce wrong calls. Precise descriptions specify what the tool returns, what edge cases exist, and what formats or units the arguments expect. The description is the contract β€” treat it that way.

Keep Tools Focused and Composable

A single database_query tool that accepts arbitrary SQL is harder for Claude to use correctly than list_users, get_user_by_id, and search_users_by_email. Narrow tools produce better results because Claude can express intent precisely. They’re also easier to test and audit in isolation.

Log the Full Trace

Tool calls are side effects. When an agent does something unexpected, you need the full trace: tool name, inputs, output, timestamp, and which message triggered the call. JSON Lines appended per tool call works well β€” query with jq later to understand agent behavior patterns over time.

Constrain Scope via System Prompts

Tell Claude in the system prompt what it’s allowed to do and what it isn’t: β€œYou are a read-only research assistant. Never call write_file unless the user explicitly asks for a saved output.” Claude respects system-level constraints reliably. This is much cleaner than trying to validate tool calls inside your dispatcher.

Wiring Tool Use into n8n

If you’re running n8n on your local infrastructure (covered in my n8n + Claude automation workflows guide), the cleanest pattern is: expose your tool dispatcher as a FastAPI service, then define Claude tools that call its endpoints over localhost. Your n8n workflows trigger the agent via webhook or schedule, the agent calls your FastAPI tools, and results flow back through n8n for further processing β€” Slack notifications, database writes, email reports, whatever the downstream workflow needs. This keeps the agentic logic in Python where it’s easy to test, while n8n handles the trigger, notification, and routing layer it’s purpose-built for.

What to Build Next

Tool use is the primitive everything else builds on. Once you’re comfortable with the loop pattern here, the interesting territory opens up:

  • RAG + tools: Add a search_knowledge_base tool backed by a vector store so Claude retrieves relevant context before doing anything else.
  • Multi-agent coordination: Build a coordinator agent that spawns specialized sub-agents with different tool sets β€” one for web research, one for code generation, one for output formatting.
  • Vision tools: Give Claude a take_screenshot tool and build UI automation without the full computer-use API overhead.
  • Prompt caching: If your tool definitions are long and stable, cache them with the cache_control beta header and cut token costs significantly on repeated calls.

The fundamentals here β€” define tools in JSON Schema, loop until end_turn, dispatch in parallel where possible, handle errors with structured output, guard with a max iteration count β€” apply across all of it. Get this pattern solid and everything else is a variation on the theme. All code in this post is tested against claude-sonnet-4-5 using the anthropic Python SDK 0.34+.

πŸ“¬Weekly Newsletter

Get the best home lab & AI content

No spam. One email per week. Unsubscribe anytime.

Share this article