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

Structured output: a typed ticket

Give an agent an output_type and the run ends with an instance of it, checked by Pydantic, instead of text you would have to parse.

Example
from typing import Literal

from pydantic import BaseModel, Field


class Ticket(BaseModel):
    category: Literal["billing", "shipping", "other"]
    priority: int = Field(ge=1, le=5, description="1 is low, 5 is urgent")
Example
agent = Agent(shop_model, output_type=Ticket)
result = agent.run_sync("I was charged twice for one order")
print(result.output)
print(type(result.output).__name__)
print(result.output.priority + 1)

result.output is a Ticket, so result.output.priority is an int you can do arithmetic with, and your editor knows its fields.

How the model is asked for a Ticket

A model only produces text and tool calls. Pydantic AI turns Ticket into a tool the model must call to finish, and the tool's arguments are the ticket's fields. A model function can print what it was given:

Example
def peek(messages, info):
    tool = info.output_tools[0]
    print("output tool:", tool.name)
    print("text allowed:", info.allow_text_output)
    print(json.dumps(tool.parameters_json_schema, indent=2))
    return ModelResponse(parts=[ToolCallPart(tool.name, {"category": "billing", "priority": 4})])


Agent(FunctionModel(peek), output_type=Ticket).run_sync("I was charged twice")
  • The output tool is called final_result. Calling it ends the run.
  • text allowed: False: with an output_type that is not str, a plain text answer does not end the run.
  • The schema is Pydantic's JSON Schema for Ticket: the Literal became an enum, and Field's limits and description are there for the model to read.

This way of getting data, through a tool call, is the default because almost every model supports tools. NativeOutput and PromptedOutput from pydantic_ai use a provider's JSON mode or plain instructions instead, for models where that works better.

Not only models

Example
agent = Agent(shop_model, output_type=list[str])
print(agent.run_sync("hello").output)

Any type Pydantic can validate works: int, list[str], a TypedDict. A type that is not an object, like list[str], is wrapped in an object with one field named response. The stand-in knows nothing about that and filled in category and priority, so validation failed. What happened next is lesson 7.

Try it yourself
  • Add summary: str to Ticket. What does the stand-in's output fail on?
  • Change category to a plain str and print the schema again.
  • Print result.all_messages()[-1] after a typed run.

Little by little, you're building something great.