Dependencies: giving tools your data
A dependency is an object a single run needs, such as the current customer and their orders. You pass it to the run, and tools read it from a RunContext, so it never reaches the model.
Last updated: 28 Sep, 2026 · Pydantic AI 2.51
The tool in the last lesson read a module-level dictionary. That is fine for a demo and wrong for a real desk, where each request is a different customer whose data must not leak into another run.
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")
Declaring the dependency type
A dataclass holds what one run needs. deps_type=Desk tells the agent, and your type checker, what to expect.
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
from shop_model import shop_model
@dataclass
class Desk:
customer: str
orders: dict[str, str]
agent = Agent(shop_model, deps_type=Desk)Reading deps in a tool
@agent.tool, not tool_plain, passes a RunContext as the first argument. ctx.deps is the object given to this run, and ctx is not part of the tool's schema, so the model never sees it.
@agent.tool
def lookup_order(ctx: RunContext[Desk], order_id: str) -> str:
"""Look up the status of one of this customer's orders."""
return ctx.deps.orders.get(order_id, "not one of this customer's orders")One agent, two customers
Give each run its own Desk. Each looks only at the orders it was handed.
asha = Desk(customer="Asha", orders={"A-1001": "shipped on 12 March"})
ravi = Desk(customer="Ravi", orders={"A-1002": "waiting for stock"})
print(agent.run_sync("Where is A-1001?", deps=asha).output)
print(agent.run_sync("Where is A-1001?", deps=ravi).output)Order A-1001: shipped on 12 March. Order A-1001: not one of this customer's orders.
Ravi cannot read Asha's order even by quoting its number, because his run was never given it. With a global dictionary, every run would see every order.
Building instructions from deps
Instruction functions can take RunContext too, so the system prompt can name the customer. Here a spy model returns the instructions it was sent.
def spy(messages, info):
return ModelResponse(parts=[TextPart(info.instructions)])
agent = Agent(FunctionModel(spy), deps_type=Desk, instructions="You answer support tickets.")
@agent.instructions
def customer(ctx: RunContext[Desk]) -> str:
return f"The customer is {ctx.deps.customer}. Orders on file: {len(ctx.deps.orders)}."
print(agent.run_sync("hi", deps=Desk("Asha", {"A-1001": "shipped"})).output)You answer support tickets. The customer is Asha. Orders on file: 1.
Besides deps, RunContext carries the run's usage, its messages and the current retry count. Output validators and output functions can take it as well.
Global data vs a dependency
| A module global | A dependency | |
|---|---|---|
| Set per run | No, shared by every run | Yes, passed to each run |
| Isolated between customers | No | Yes |
| Swappable in a test | Hard, needs patching | Pass a different object |
When you reach for a dependency
- A per-request identity: the signed-in customer, their tier, their locale.
- A client or connection the tools call: a database, an HTTP session, a cache.
- Configuration that changes per run: a refund limit, a feature flag.
Desk with made-up orders and the agent code stays the same.@agent.tool takes ctx as its first argument; @agent.tool_plain does not. Use tool_plain and try to read ctx.deps and you get a plain NameError, because there is no context parameter to read from.Related
- Previous: Function tools: letting the model look things up
- Next: Tool errors: ModelRetry and crashes
- Reference: Dependencies
- Run without
deps=and read the error the run raises. - Add
refund_limit: floattoDeskand mention it in the instructions. - Print
ctx.usage.requestsinsidelookup_order.
You understood something today that you didn't yesterday.