Beyond type validation: business-rule validation
Lesson 3's output_type catches shape problems, a missing field, a string where a float was expected, automatically. But some rules aren't about shape, they're about meaning: "the number must be positive," "the date must be in the future," "the email domain must be one we allow." @agent.output_validator is where those rules go.
from dotenv import load_dotenvfrom pydantic_ai import Agent, ModelRetry, RunContext
load_dotenv()
agent = Agent("google:gemini-3.5-flash-lite", output_type=int)
@agent.output_validatordef must_be_positive(ctx: RunContext[None], output: int) -> int: if output <= 0: raise ModelRetry("The number must be positive. Try again.") return output
def main() -> None: result = agent.run_sync("Give me a positive integer between 1 and 10.") print("Output:", result.output) print("Output type:", type(result.output))An output validator receives the already-type-validated output and either returns it, possibly transformed, for example normalizing whitespace, or raises ModelRetry with a message. That message goes straight back to the model as feedback, and Pydantic AI asks it to produce another answer, same as an automatic retry from a type-validation failure in Lesson 3, just triggered by your own logic instead of Pydantic's.
Why raise instead of returning an error value
Raising ModelRetry keeps the "give the model another chance" flow uniform: type failures and business-rule failures both retry the same way, up to the agent's retries limit, and both ultimately either succeed or raise a real exception you have to handle. There's no silent "returned None on failure" path to forget to check.
Checkpoint
@agent.output_validatorruns after type validation, for rules about meaning, not shape.- Raise
ModelRetry("...")to send feedback back to the model and get another attempt. - Type-validation retries (Lesson 3) and output-validator retries share the same retry budget and the same underlying mechanism.
If anything here still feels unclear, ask before moving to Lesson 9, where output starts streaming instead of arriving all at once.