Pydantic AI: Typed, Testable Agents for Engineers Who Like Guarantees
Book: Agents in Production — Building, Tracing, and Shipping Multi-Step AI You Can Trust Also by me: Observability for LLM Applications — the companion book in The AI Engineer's Library (2-book series) My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools Me: xgabriel.com | GitHub You ship an agent that resolves billing disputes. It works in the demo. Two weeks later a support ticket lands: the agent tried to refund $4,000 on a $19 charge. You read the trace. The model returned a JSON blob, your code did json.loads , pulled amount , and passed it straight to the payments API. No cap. No type. No check. The model hallucinated a number and your code trusted it. The model is stochastic. Your code does not have to be. The gap between those two facts is where most production agent bugs live, and it is exactly the gap Pydantic AI is built to close. The wedge is types Most agent frameworks hand you an Agent object and a bag of strings. Pydantic AI hands you Agent[Deps, Output] — a generic parameterized by its dependency type and its output type. The IDE and your type checker read those parameters. So does the runtime. Install pulls in the framework plus an optional tracing extra: pip install "pydantic-ai[logfire]" The smallest program that earns its keep: from dataclasses import dataclass from pydantic import BaseModel from pydantic_ai import Agent , RunContext @dataclass class Deps : customer_name : str class SupportReply ( BaseModel ): reply : str escalate : bool agent = Agent ( " anthropic:claude-opus-4-8 " , deps_type = Deps , output_type = SupportReply , system_prompt = " You are a support agent. " , ) A tool is a plain function whose type hints become the schema the model sees, and the run returns the validated SupportReply : @agent.tool def customer_name ( ctx : RunContext [ Deps ]) -> str : return ctx . deps . customer_name result = agent . run_sync ( " What is my name? " , deps = Deps ( customer_name = " Ana "