Claude Best Practices

Claude Function Calling Mastery: Dynamic Tools for Python AI Agents

Supercharge your Claude AI agents with dynamic function calling. Master adaptive tools, error handling, and Python SDK patterns for production-ready architectures.

A

Andrew Snyder

AI & Automation Editor

December 12, 2025 min read
Share:

Why Function Calling is a Game-Changer for Claude Agents

Hey there, Claude enthusiasts! If you're building AI agents that need to interact with the real world—think fetching data, running calculations, or integrating with APIs—function calling (or tool use in Claude lingo) is your secret weapon. Claude's tool capabilities shine in models like Opus and Sonnet, letting your agents dynamically select and execute tools based on context. In this guide, we'll go beyond basics to master dynamic tools, smart error handling, and adaptive selection, all with battle-tested Python code using the Anthropic SDK.

Whether you're a dev crafting enterprise agents or a business user automating workflows, these techniques will make your agents smarter, more reliable, and scalable. Let's dive in!

Quick Recap: Claude's Function Calling Basics

Claude supports tool use via the Anthropic API, where you define JSON schemas for functions/tools, and Claude decides when to call them. Key perks over plain prompting:

  • Structured outputs: No more regex parsing hallucinations.
  • Dynamic invocation: Claude picks the right tool automatically.
  • Stateful agents: Loop tools with conversation history.

Supported in Claude 3 family (Haiku for speed, Sonnet/Opus for complexity). Start with the Python SDK:

pip install anthropic

Basic setup:

import anthropic
import os

client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

Defining Tools: The Schema Blueprint

Tools are defined as JSON schemas. Claude expects an array of tools with name, description, input_schema, and optional type: "function".

Pro tip: Rich descriptions help Claude select tools accurately—think of it as prompt engineering for tools.

Here's a simple calculator tool:

calculator_tool = {
    "name": "calculator",
    "description": "Perform basic math calculations",
    "input_schema": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Math expression like '2 + 2 * 3'"
            }
        },
        "required": ["expression"]
    }
}

# Usage in API call
message = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    max_tokens=1024,
    tools=[calculator_tool],
    messages=[{"role": "user", "content": "What's 15% of 200?"}]
)

Claude responds with tool_calls if it needs the tool, including name and input args. Your agent then executes and appends the result as a tool_result.

Level Up: Dynamic Tool Selection

Static tools? Boring. Dynamic selection lets Claude pick from a toolbox based on the query. Use tool_choice: {"type": "auto"} (default) for magic.

For adaptive agents, maintain a tool registry—a dict of available tools loaded dynamically:

TOOL_REGISTRY = {
    "calculator": calculator_tool,
    "weather": {
        "name": "get_weather",
        "description": "Fetch current weather for a city",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {"type": "string"}
            },
            "required": ["city"]
        }
    }
    # Add more dynamically, e.g., from config or MCP servers
}

def get_tools_for_context(context):
    # Adaptive: Filter tools based on user query
    if "weather" in context.lower():
        return [TOOL_REGISTRY["weather"]]
    return [TOOL_REGISTRY["calculator"]]  # Default

In your agent loop:

tools = get_tools_for_context(user_query)
response = client.messages.create(
    model="claude-3-5-sonnet-20240620",
    tools=tools,
    tool_choice="auto",
    messages=messages
)

Claude auto-selects! For advanced agents, use conditional tool sets—e.g., enterprise tools only after auth.

Bulletproof Error Handling

Agents fail gracefully or die trying. Common pitfalls:

  • Invalid tool args (Claude hallucinates schemas)
  • Execution errors (API downtime)
  • Infinite loops (tool calls forever)

Strategy 1: Validate Inputs

from pydantic import BaseModel, ValidationError

class CalcInput(BaseModel):
    expression: str

# In executor
def safe_execute(tool_call):
    try:
        if tool_call.name == "calculator":
            args = CalcInput(**tool_call.input)
            result = eval(args.expression)  # WARNING: Use safe eval in prod!
            return {"content": str(result)}
    except ValidationError as e:
        return {"content": f"Input error: {e}", "is_error": True}
    except Exception as e:
        return {"content": f"Tool failed: {str(e)}", "is_error": True}

Strategy 2: Retry Logic with Exponential Backoff

import time

def agent_loop(messages, max_iters=5):
    for i in range(max_iters):
        response = client.messages.create(..., messages=messages)
        if not response.stop_reason == "tool_use":
            return response.content[0].text
        
        for tool_call in response.tool_calls:
            tool_result = safe_execute(tool_call)
            messages.append({
                "role": "assistant", "content": [], "tool_calls": [tool_call]
            })
            messages.append({"role": "tool", "content": [tool_result], "tool_call_id": tool_call.id})
        
        if i == max_iters - 1:
            messages.append({"role": "user", "content": "Max iterations reached. Summarize."})
    return client.messages.create(..., messages=messages)

Strategy 3: Error Feedback Loop Feed errors back to Claude: "Tool failed with X. Try another approach?" Claude adapts!

Building a Robust Python AI Agent

Time for the full monty: A multi-tool agent with dynamic selection, errors, and persistence.

import json
from typing import List, Dict

class ClaudeAgent:
    def __init__(self, model="claude-3-5-sonnet-20240620"):
        self.client = anthropic.Anthropic()
        self.model = model
        self.messages: List[Dict] = []
        self.tool_registry = TOOL_REGISTRY

    def add_system_prompt(self, prompt: str):
        self.messages.insert(0, {"role": "system", "content": prompt})

    def run(self, query: str, available_tools: List[str] = None) -> str:
        self.messages.append({"role": "user", "content": query})
        
        tools = [self.tool_registry[name] for name in (available_tools or self.tool_registry.keys())]
        
        while True:
            response = self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                messages=self.messages,
                tools=tools,
                tool_choice="auto"
            )
            
            self.messages.append(response.content[0] if response.content else {})
            
            if response.stop_reason != "tool_use" or not response.tool_calls:
                return response.content[0].text
            
            for tool_call in response.tool_calls:
                tool_result = self.safe_execute(tool_call)
                self.messages.append({
                    "role": "tool",
                    "content": [tool_result],
                    "tool_call_id": tool_call.id
                })

    def safe_execute(self, tool_call):
        # Implementation as above
        pass

# Usage
agent = ClaudeAgent()
agent.add_system_prompt("You are a helpful assistant with tools. Use them wisely.")
result = agent.run("What's the weather in NYC and tip 15% on a $50 bill?")
print(result)

This agent handles mixed queries: Claude calls get_weather + calculator dynamically!

Real-World Example: Sales Lead Qualifier Agent

Imagine an agent for sales: Tools for CRM lookup, email validation, sentiment analysis.

Extend TOOL_REGISTRY:

crm_lookup = {
    "name": "crm_lookup",
    "description": "Query CRM for lead by email",
    "input_schema": {"type": "object", "properties": {"email": {"type": "string"}}, "required": ["email"]}
}

# Mock executor
def mock_crm(email):
    return {"name": "John Doe", "score": 0.8}  # Integrate with HubSpot/Salesforce

agent.run("Qualify lead: john@example.com. Is it hot?")

Claude: Calls CRM → Analyzes score → Responds: "Hot lead! Score 0.8, suggest call."

Integrate with n8n/Zapier? Pipe tool results via webhooks.

Best Practices for Claude Function Calling

  • Schema Precision: Use Pydantic for validation; keep schemas simple (Claude chokes on nested objects).
  • Token Limits: Tools eat tokens—use Haiku for cheap iterations, Sonnet for reasoning.
  • Parallel Calls: Claude supports multiple tool_calls in one response—execute in parallel with asyncio.
  • MCP Integration: Pair with Model Context Protocol servers for external tools (e.g., browser control).
  • Monitoring: Log stop_reason, tool success rates. Use Anthropic's console.
  • Security: Sanitize inputs; never eval user math in prod (use sympy).
  • Testing: Mock executors for unit tests.

Scaling to Production

For enterprise:

  • Async Agents: Use asyncio.gather for parallel tools.
  • State Management: Redis for multi-turn sessions across Claude Code or API.
  • Rate Limits: Implement queues (Anthropic: 50 RPM for Sonnet).

Compare to GPT: Claude's tool_use is more reliable on complex reasoning, less hallucination on schemas.

Wrapping Up

You've got the blueprint for masterful Claude agents! From dynamic selection to resilient error handling, these patterns turn prompts into powerhouses. Fork the code, tweak for your stack (Slack bots? Engineering playbooks?), and share your wins in the comments.

Next up: MCP servers for infinite tools. Stay tuned on Claude Directory.

Word count: ~1450

The #1 Newsletter in AI

Stay ahead of the AI curve

The most important updates, news, and content — delivered in one weekly newsletter.

No spam. Unsubscribe anytime. Privacy policy

claude api
function calling
ai agents
python sdk
anthropic sdk
ai-agents
A

About Andrew Snyder

AI & Automation Editor

Andrew covers practical AI automation, workflow design, and the tools teams use to streamline everyday operations.

Comments (0)