Function / Tool Calling Basics
Introduction
In the last three lessons you learned to make a model return structured data you can trust — valid JSON in exactly the shape you asked for. That's powerful, but the model is still only describing things. It can fill out a form; it can't do anything.
Tool calling (also called function calling) is the leap from describing to doing. You hand the model a list of functions it's allowed to use — get_weather, search_orders, send_email — and mid-conversation it can decide: "to answer this, I need to call get_weather with city="Tokyo"." Your code runs the function, hands back the result, and the model finishes its answer using real data.
This one capability is the foundation of everything 'agentic' — retrieval, browsing, calculators, database lookups, booking systems. Every agent you've heard of is, at its core, a model running a tool-calling loop.
In this lesson you'll learn:
- The single most important mental model: the model never runs your code — it only requests calls
- Why tool calling is just structured output, pointed at a function
- The full tool-calling loop, step by step (with a widget to walk through it)
- How to define a tool and run the round-trip in code (Claude + OpenAI)
- How to steer it:
tool_choice, parallel calls, and strict mode
The One Idea: The Model Doesn't Run Anything
Here is the mental model that everything else hangs on — and the one most beginners get wrong:
The model does not execute your functions. It cannot. It only outputs a request to call one — a name and some arguments — as structured JSON. Your code decides whether and how to actually run it.
When you give Claude or GPT a get_weather tool, the model has no network access, no Python interpreter, no database connection. What it produces is literally this:
{ "name": "get_weather", "input": { "city": "Tokyo" } }
That's it — a piece of JSON saying "I'd like you to call this." Whether that becomes a real API request, a mocked value, a permission prompt, or nothing at all is 100% up to your code. The model proposes the call; your program disposes.
Why this matters for you:
- Security and control live entirely on your side. The model can ask to call
delete_account, but it can't make it happen — you choose what each tool actually does (and whether to require a human confirmation first). - Debugging gets simpler once this clicks: a 'tool call' is just JSON the model emitted. Wrong arguments? That's a prompt/schema problem, not a mysterious runtime bug.
- It's why tool calling is safe by default: nothing runs that you didn't write.
It's Structured Output, Aimed at a Function
You already know how this works under the hood — you just learned it.
In the structured-output lessons, the model used constrained decoding to emit JSON matching a schema you defined. Tool calling is that exact machinery, with one twist: the schema describes a function's arguments, and the model also picks which function.
So a tool call is two structured decisions:
- Which tool? (or none — just answer normally)
- What arguments? — emitted as JSON that conforms to that tool's input schema
That's why tool arguments are as reliable as structured output: with strict/constrained tool schemas, the input is guaranteed to match the shape you specified (recall JSON Mode & Response Schemas). The model can still choose wrong values — schema guarantees shape, not correctness — but it can't hand you malformed arguments.
The practical upshot: a good tool is a good schema plus a good description. The whole next lesson is about designing them well.
The Tool-Calling Loop
Tool calling isn't a single request — it's a loop between your code and the model. Here's the whole thing, start to finish:
- You → model: the user's message plus your list of tool definitions.
- Model → you: instead of a final answer, it returns a tool-call request and stops. The API flags this explicitly —
stop_reason: "tool_use"(Claude) orfinish_reason: "tool_calls"(OpenAI). - You execute: your code reads the requested name + arguments and runs the real function.
- You → model: you append the result to the conversation and call the model again.
- Model → you: now holding real data, it writes the final answer — or requests another tool, and the loop repeats.
The loop can run many times (search, then calculate, then…) until the model has what it needs. Step through one round below:


Defining a Tool
A tool definition is three things: a name, a description, and an input schema (JSON Schema for its arguments). The description is not a code comment — it's the prompt the model reads to decide when and how to use the tool, so it earns real care (that's the next lesson).
# A tool definition — Claude (Anthropic) format
tools = [
{
"name": "get_weather",
"description": (
"Get the current weather for a city. "
"Call this whenever the user asks about weather, temperature, or rain."
),
"input_schema": { # ← JSON Schema for the arguments
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Tokyo' or 'Sao Paulo'",
}
},
"required": ["city"],
},
}
]OpenAI uses the same pieces, named slightly differently — and its newest Responses API flattens the shape (no nested function key):
# The same tool — OpenAI Responses API format
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get the current weather for a city. Call this when the user asks about weather.",
"parameters": { # ← OpenAI calls the schema 'parameters'
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Tokyo'"}
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True, # guarantee the args match the schema
}
]
# (The older Chat Completions API nests these fields under a "function" key.)The Round-Trip in Code
Now the full loop in code. Watch the four moves: send with tools → detect the tool request → run the function → send the result back, then read the final answer.
import anthropic
client = anthropic.Anthropic()
def get_weather(city: str) -> dict: # YOUR real function
return {"temp_c": 17, "condition": "light rain"} # (a real call would hit an API)
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
# 1. Send the message WITH the tools
resp = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages,
)
# 2. Did the model ask for a tool?
if resp.stop_reason == "tool_use":
call = next(b for b in resp.content if b.type == "tool_use")
# 3. Run the real function with the model's arguments (already parsed for you)
result = get_weather(**call.input) # call.input == {"city": "Tokyo"}
# 4. Send the result back, tagged with the SAME tool id
messages.append({"role": "assistant", "content": resp.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": call.id,
"content": str(result),
}],
})
final = client.messages.create(
model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages,
)
print(final.content[0].text)
# → "It's 17°C with light rain in Tokyo right now."The OpenAI flow is the same five steps with three dialect differences worth memorising:
| Claude | OpenAI | |
|---|---|---|
| "I want a tool" signal | stop_reason == "tool_use" | finish_reason == "tool_calls" |
| Arguments arrive as | a parsed dict (call.input) | a JSON string you must json.loads() |
| You return the result as | a tool_result block | role: "tool" (Chat) / function_call_output (Responses) |
That JSON-string gotcha bites everyone once: OpenAI hands you arguments as text, so parse it before calling your function.
# OpenAI (Chat Completions): the result-handling differences
import json
msg = resp.choices[0].message
if msg.tool_calls: # finish_reason == "tool_calls"
call = msg.tool_calls[0]
args = json.loads(call.function.arguments) # ← arguments is a STRING; parse it
result = get_weather(**args)
messages.append(msg) # the assistant's tool request
messages.append({ # the result, tagged by id
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
# ...call the model again to get the final answerSteering It: tool_choice, Parallel Calls, Strict
Three controls turn tool calling from a demo into something production-ready.
tool_choice — who decides whether to call a tool?
auto(default) — the model decides per turn: call a tool or just answer. Right for almost everything.any(Claude) /required(OpenAI) — the model must call some tool (it can't reply in plain text). Use when a tool call is the whole point (e.g. "always extract into this function").- Force a specific tool —
{"type":"tool","name":"get_weather"}(Claude) /{"type":"function","name":"get_weather"}(OpenAI). Handy for tests or a fixed first step. none— tools visible but forbidden this turn.
Parallel tool calls. When a request needs several independent lookups, the model can return multiple tool calls at once (e.g. weather for Tokyo and London). Run them all, return all results. Force one-at-a-time with parallel_tool_calls=False (OpenAI) / disable_parallel_tool_use=True (Claude).
Strict mode (OpenAI). strict: true on a tool guarantees the arguments conform to your schema via constrained decoding — the structured-output guarantee, applied to tool arguments. Make it your default.
🧪 Try It Yourself
Predict the loop. A user asks your assistant: "Compare the weather in Tokyo and London and tell me which is warmer." Your assistant has exactly one tool: get_weather(city).
- How many times must the model hand control back to your code before it can answer? (Hint: it needs two facts.)
- Will it likely use parallel tool calls or sequential ones — and why?
- At which step does the model finally write the comparison sentence?
→ It needs get_weather for two cities, so expect two tool calls — often parallel, since neither depends on the other → your code runs both → you return both results → then the model writes the comparison. The model never compares until it's holding both real numbers. Wire this up with mocked weather values and watch stop_reason flip from tool_use to end_turn on the final turn.
Mental-Model Corrections
- "The model runs the tool." No — it only emits a JSON request; your code runs everything. The entire security model depends on this.
- "The arguments are always right." The shape is guaranteed (with strict schemas); the values are not. The model can pass
city="Tokoyo"or invent a parameter — validate inputs before acting on anything that matters. - "OpenAI gives me a dict." It gives you a JSON string in
arguments—json.loads()it first. (Claude gives a parsed dict.) - "I can skip returning a result." You can't — every tool call the model makes must get a matching result (by id) appended before the next request, or the API errors. If the call failed, return an error message as the result; don't drop it.
- "Forcing a tool is always safer." Forcing (
any/required) removes the model's option to just answer — great for extraction, wrong for a general assistant that should sometimes reply in plain text.
Key Takeaways
- Tool calling = the model requests a function; your code runs it. The model only ever outputs structured JSON (name + arguments).
- It's structured output pointed at a function — the same constrained-decoding guarantee, now also choosing which tool.
- The loop: send message + tools → model returns a tool call (
stop_reason/finish_reason) → you execute → return the result by id → model answers (or calls another tool). - Dialect differences:
tool_usevstool_calls; parsed dict vs JSON-string arguments;tool_resultvsrole:"tool". - Steer with
tool_choice(auto/any/force/none), parallel calls, and strict schemas. Always validate arguments — shape is guaranteed, correctness isn't. - This loop is the foundation of every agent. Next: designing tool schemas the model actually uses well.