Tool calling is a contract between your code and the LLM: you declare a list of tools with descriptions and argument schemas, the model decides which to call, and your code does the real work.
The model does not execute code. It generates a JSON request — you run it.
tool = {
"name": "get_stock_price",
# The description is the PRIMARY signal for when to call this tool
"description": (
"Get the current stock price for a ticker symbol. "
"Use when the user asks about stock prices or market data."
),
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker, e.g. 'AAPL', 'MSFT'"
},
"currency": {
"type": "string",
"enum": ["USD", "EUR"],
"description": "Currency for the price"
}
},
"required": ["ticker"]
}
}
Good description rules:
- Start with a verb: "Get", "Search", "Send", "Create".
- State the trigger: "Use when the user asks about…".
- State limitations: "Only for public companies".
Full Cycle with the Anthropic SDK
import anthropic, json
client = anthropic.Anthropic()
def get_stock_price(ticker: str, currency: str = "USD") -> dict:
prices = {"AAPL": 189.5, "MSFT": 415.2}
return {"ticker": ticker, "price": prices.get(ticker.upper(), 0), "currency": currency}
tools = [{
"name": "get_stock_price",
"description": "Get current stock price. Use when user asks about stock prices.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {"type": "string"},
"currency": {"type": "string", "enum": ["USD", "EUR"]}
},
"required": ["ticker"]
}
}]
messages = [{"role": "user", "content": "What are Apple and Microsoft stock prices?"}]
response = client.messages.create(model="claude-opus-4-5", max_tokens=1024, tools=tools, messages=messages)
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = get_stock_price(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": f"Error: {e}",
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
final = client.messages.create(model="claude-opus-4-5", max_tokens=1024, tools=tools, messages=messages)
print(final.content[0].text)
When the user asks about multiple tickers, Claude returns multiple tool_use blocks in one response. Process all of them before the next request:
for block in response.content:
if block.type == "tool_use":
# Could be two calls: AAPL and MSFT simultaneously
result = get_stock_price(**block.input)
tool_results.append({...})
Parallel calls reduce latency — instead of two sequential API calls you make one.
Error Handling
| Situation | What to do |
|-----------|-----------|
| Tool raised an exception | Return is_error: true with the error text |
| Tool returned empty result | Return "No results found" — model adjusts |
| Invalid args from the model | Validate before calling, return validation error |
Never silently ignore tool errors — the model won't know something went wrong and will confidently give a wrong answer.