Function tools: letting the model look things up
A function tool is a Python function the model can choose to call during a run. Pydantic AI builds the tool's description from your type hints and docstring, and runs the function when the model asks for it.
Last updated: 28 Sep, 2026 · Pydantic AI 2.51
A model on its own can only write text. A tool lets it fetch a real value first, like an order's status, and answer from what came back.
Storing the order data
Start with a small table of orders the tool can read. In a real desk this would be a database call.
ORDERS = {
"A-1001": "shipped on 12 March",
"A-1002": "waiting for stock",
}Writing the lookup tool
@agent.tool_plain registers a plain function, one that needs nothing from the run itself. The docstring's Args: section describes each argument to the model.
@agent.tool_plain
def lookup_order(order_id: str) -> str:
"""Look up the status of an order.
Args:
order_id: The order id, like A-1001.
"""
return ORDERS.get(order_id, "not found")View the code here
import re
from pydantic_ai import ModelResponse, TextPart, ToolCallPart
from pydantic_ai.models.function import AgentInfo, FunctionModel
def sort_ticket(text):
text = text.lower()
if "charged" in text or "refund" in text:
return "billing", 4
if "parcel" in text or "arrived" in text:
return "shipping", 3
return "other", 1
def shop_reply(messages, info: AgentInfo) -> ModelResponse:
prompts = [p.content for m in messages for p in m.parts if p.part_kind == "user-prompt"]
ticket = prompts[-1]
last = messages[-1].parts[-1]
order = re.search(r"A-\d{4}", ticket)
# 1. The ticket names an order and the agent has a tool: ask for it.
if order and info.function_tools and last.part_kind == "user-prompt":
tool = info.function_tools[0].name
return ModelResponse(parts=[ToolCallPart(tool, {"order_id": order.group()})])
# 2. A tool answered: write the reply from what it said.
if last.part_kind == "tool-return" and info.allow_text_output:
return ModelResponse(parts=[TextPart(f"Order {order.group()}: {last.content}.")])
# 3. The agent wants a typed answer: fill in its output tool.
category, priority = sort_ticket(ticket)
if info.output_tools:
args = {"category": category, "priority": priority}
return ModelResponse(parts=[ToolCallPart(info.output_tools[0].name, args)])
# 4. Otherwise, plain text.
return ModelResponse(parts=[TextPart(f"Sorted as {category}.")])
shop_model = FunctionModel(shop_reply, model_name="shop")
Calling the tool in a run
Ask about an order the table knows. The stand-in model reads the order id from the ticket, calls the tool, and writes its reply from the result.
result = agent.run_sync("Where is my order A-1001?")
print(result.output)
print(result.usage)Order A-1001: shipped on 12 March. RunUsage(input_tokens=114, output_tokens=17, requests=2, tool_calls=1)
What the tool call cost
- The answer came from the tool: the model turned
shipped on 12 Marchinto a sentence. - requests=2 because the run went to the model twice: once to get the tool call, once to write the reply after the result came back.
- tool_calls=1 records the single call to
lookup_order.
What the model reads about a tool
The model never sees your function body. It sees a schema built from the name, the docstring and the type hints. Print it to see what the model chooses from.
def peek(messages, info):
tool = info.function_tools[0]
print(tool.name)
print(tool.description)
print(json.dumps(tool.parameters_json_schema, indent=2))
return ModelResponse(parts=[TextPart("ok")])
agent = Agent(FunctionModel(peek))
agent.tool_plain(lookup_order)
agent.run_sync("hi")lookup_order
Look up the status of an order.
{
"additionalProperties": false,
"properties": {
"order_id": {
"description": "The order id, like A-1001.",
"type": "string"
}
},
"required": [
"order_id"
],
"type": "object"
}The name is the function's name, the description is the docstring, and the Args: line became the description of order_id. Pydantic AI reads Google, NumPy and Sphinx docstring styles. The model picks tools from these words alone, so they are worth writing well.
Three ways to register a tool
| How | When to use it |
|---|---|
@agent.tool_plain | A function that needs no run context, defined next to the agent. |
agent.tool_plain(fn) | The same, without the decorator, for a function defined elsewhere. |
Agent(..., tools=[fn]) | Pass a list of functions when you create the agent. |
Arguments are validated before your function runs: Pydantic checks them against the type hints, and a wrong type goes back to the model as a retry, the same way a bad structured output does. A function with order_id: str can rely on getting a string.
When you reach for a tool
- Looking a value up: an order's status, a customer's plan, stock on hand.
- Doing something with an effect: send an email, open a ticket, start a refund.
- Fetching fresh data the model cannot know: today's price, the current queue length.
Related
- Previous: Output functions and several output types
- Next: Dependencies: giving tools your data
- Reference: Function tools
- Add
refund_order(order_id: str, amount: float) -> strand print both tools' schemas. - Ask about
A-1002, then aboutA-5555, and read each answer. - Remove the docstring and print the description again.
Little by little, you're building something great.