Pydantic AIPydantic AI 2.51 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
29 small wins to finish your pathNext lesson →

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.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
shop_model.py
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.

python
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.

python
@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.

Example
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)

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.

Example
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)

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 globalA dependency
Set per runNo, shared by every runYes, passed to each run
Isolated between customersNoYes
Swappable in a testHard, needs patchingPass 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.
Why not a global
In a web app each request builds its own dependencies, so one customer's data never leaks into another's run. In a test you pass a Desk with made-up orders and the agent code stays the same.
Watch out. @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.
Try it yourself
  • Run without deps= and read the error the run raises.
  • Add refund_limit: float to Desk and mention it in the instructions.
  • Print ctx.usage.requests inside lookup_order.

You understood something today that you didn't yesterday.