Wiring up python claude messages api function calling is the fastest way to let Claude invoke your own code during a conversation. This tutorial builds a runnable example that defines a weather tool, streams the model’s tool request, executes it locally, and feeds the result back for a final answer.
Prerequisites
- Python 3.10 or newer
anthropicPython package (v0.25+)- An Anthropic API key exported as
ANTHROPIC_API_KEY - A scratch directory for a single file
claude_tools.py
pip install anthropic
export ANTHROPIC_API_KEY=sk-ant-...
No framework needed. The native SDK maps directly to the HTTP Messages API, so what you see here translates cleanly to raw requests if you ever leave the SDK behind.
Tool schema: tell Claude what it can call
Claude doesn’t guess function names. You pass a JSON schema describing each tool. The python claude messages api function calling contract expects a tools list where each entry has name, description, and input_schema (JSON Schema draft-07).
{
"name": "get_weather",
"description": "Get current temperature in Celsius for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Berlin'"
}
},
"required": ["city"]
}
}
In Python we keep this as a dict and pass it straight to the client.
First request: ask a question that needs the tool
We send a user message that implicitly requires external data. Claude decides whether to call the tool.
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
tools = [
{
"name": "get_weather",
"description": "Get current temperature in Celsius for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}
]
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the temperature in Berlin?"}],
)
print(response.stop_reason)
Expected output at this checkpoint:
tool_use
stop_reason of tool_use means Claude produced one or more tool_use content blocks instead of plain text. If you skip the tool schema, you’ll get end_turn and a text guess.
Extracting the tool call
The response content is a list of blocks. Iterate and pick type == "tool_use".
tool_use_block = next(b for b in response.content if b.type == "tool_use")
print(tool_use_block.name, tool_use_block.input)
Output:
get_weather {'city': 'Berlin'}
The input is already parsed into a Python dict matching your schema. Validate it yourself if the function is destructive; Claude respects schemas but downstream bugs happen.
Implementing the local function
We fake the weather call. In production this hits your DB or API.
def get_weather(city: str) -> str:
# Mock lookup
return f"12°C, overcast in {city}"
The python claude messages api function calling loop requires you to return a tool_result block with the same tool_use_id.
Feeding the result back
You send a new user message containing a tool_result content block. The content field of that block is a string (or list of blocks for rich output).
tool_use_id = tool_use_block.id
weather_text = get_weather(tool_use_block.input["city"])
followup = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=[
{"role": "user", "content": "What's the temperature in Berlin?"},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": weather_text,
}
],
},
],
)
print(followup.content[0].text)
Expected final output:
The temperature in Berlin is currently 12°C and overcast.
That’s the minimal round trip. Everything else is hardening.
Building a reusable loop
Hardcoding two requests breaks the moment Claude chains two tools. Write a driver that keeps going until stop_reason == "end_turn".
def run_conversation(user_prompt: str) -> str:
messages = [{"role": "user", "content": user_prompt}]
while True:
resp = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages,
)
if resp.stop_reason == "end_turn":
return "".join(b.text for b in resp.content if b.type == "text")
# Append assistant's raw content (includes tool_use blocks)
messages.append({"role": "assistant", "content": resp.content})
# Build tool_result blocks for every tool_use in this turn
results = []
for block in resp.content:
if block.type == "tool_use":
payload = get_weather(block.input["city"])
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": payload,
})
messages.append({"role": "user", "content": results})
# Example
print(run_conversation("What's the weather in Paris and Berlin?"))
If Claude emits two tool_use blocks in one response, the loop packs both results into a single user message. That’s valid and avoids extra round trips.
Handling errors and timeouts
Tool execution fails. Wrap the call and return an error string in tool_result rather than raising—Claude can often recover.
def safe_get_weather(city: str) -> str:
try:
return get_weather(city)
except Exception as e:
return f"ERROR: {e}"
# inside loop:
payload = safe_get_weather(block.input["city"])
Set max_tokens high enough for multi-step plans. If you hit the limit mid-tool-chain, stop_reason is max_tokens, not tool_use; log and retry with a fresh context window.
Streaming tool calls
For latency-sensitive UIs, use client.messages.stream(). Tool blocks arrive in the event stream just like text. You accumulate tool_use deltas, then send results as above. The python claude messages api function calling pattern is identical; only the transport differs.
with client.messages.stream(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages,
) as stream:
for event in stream:
if event.type == "content_block_delta" and event.delta.type == "tool_use_input_delta":
# accumulate input JSON
...
We won’t expand the full streaming accumulator here; the SDK docs cover it. The key point: streaming doesn’t change the tool schema or the result format.
Production notes
- Pin the model version.
claude-3-5-sonnet-20241022is stable; aliases drift. - Log the full
messagesarray on failures. The assistant turn withtool_useblocks must be sent back verbatim, or Claude loses the thread. - If you proxy through a gateway that normalizes APIs, confirm it forwards
toolsandtool_resultwithout strippingtype. Some OpenAI-compatible shims munge content blocks. - Rate limits: a tool loop can spike request count. Backoff on 429 with
retry-after.
That’s the whole mechanism. The python claude messages api function calling flow is three moving parts: declare tools, detect tool_use, return tool_result. Get those right and multi-step agents fall out naturally.