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

Validation retries: when the model gets it wrong

When a model's answer fails validation, Pydantic AI sends the error back and asks again. The model reads what was wrong and can fix it.

This model function gets the priority wrong on its first try, then corrects it once it is told:

Example
from pydantic_ai import Agent, ModelResponse, ToolCallPart
from pydantic_ai.models.function import FunctionModel


def clumsy(messages, info):
    last = messages[-1].parts[-1]
    priority = 4 if last.part_kind == "retry-prompt" else 9
    return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": priority})])
Example
agent = Agent(FunctionModel(clumsy), output_type=Ticket)
result = agent.run_sync("I was charged twice")
print(result.output)
print(result.usage.requests)

The run still ended with a valid Ticket, and it took two requests. The messages show why:

Example
for message in result.all_messages():
    print(message.kind, [part.part_kind for part in message.parts])

print(result.all_messages()[2].parts[0].model_response())

The second request holds a retry-prompt part. model_response() is the text the model reads: Pydantic's validation error as JSON, with the field, the rule and the bad value, then Fix the errors and try again. A real model reads it the same way. The last request, tool-return, is the agent confirming the output tool call, so the message list stays valid if the conversation continues.

When it never gets it right

Example
def stubborn(messages, info):
    return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": 9})])


agent = Agent(FunctionModel(stubborn), output_type=Ticket)
agent.run_sync("I was charged twice")

By default the model gets one retry. After that the run raises UnexpectedModelBehavior, from pydantic_ai. The last validation error is attached as its cause, which is why Python prints it first. Catch it where your app can fall back, for example by sending the ticket to a person.

Example
attempts = iter([9, 7, 4])


def slow_learner(messages, info):
    return ModelResponse(parts=[ToolCallPart("final_result", {"category": "billing", "priority": next(attempts)})])


agent = Agent(FunctionModel(slow_learner), output_type=Ticket, retries=2)
result = agent.run_sync("I was charged twice")
print(result.output, result.usage.requests)

retries=2 allows two corrections, and this model needs both. Each retry is another paid request, so a high number hides a bad prompt or schema instead of fixing it.

Try it yourself
  • Make clumsy send "category": "refunds" first and read the retry prompt.
  • Set retries=0 on the first agent.
  • Send a string, "high", as the priority and read the error type.

You understood something today that you didn't yesterday.