n4nAI

Python function calling with Claude via the Messages API

Step-by-step tutorial for implementing python claude messages api function calling with the Anthropic SDK, from tool schema to multi-turn tool result loops.

n4n Team3 min read596 words

Audio narration

Coming soon — every post will get a voice note here.

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
  • anthropic Python 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-20241022 is stable; aliases drift.
  • Log the full messages array on failures. The assistant turn with tool_use blocks must be sent back verbatim, or Claude loses the thread.
  • If you proxy through a gateway that normalizes APIs, confirm it forwards tools and tool_result without stripping type. 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.

Tagspythonclaudemessages-apifunction-calling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All function calling in python posts →