Why We Brought This Tool Into Our Lab
We brought Pydantic AI into our lab because the repetitive parts of agent development were consuming more engineering time than the agent logic itself.
Types Guard the Boundary, Not the Whole Architecture
Pydantic AI validated tool arguments before execution and forced the final response into a typed output object, but it could not guarantee the model chose the right tool or made a sound business decision, so authorization, idempotency, and durable execution still belong in application code.
Our Python services already used Pydantic to define data structures and check incoming requests, business data, settings, and outgoing responses. An application programming interface, or API, lets other software exchange requests and responses with a service. Adding a large language model, or LLM, required more supporting code. We extracted JavaScript Object Notation, or JSON, a text format for structured data. We also checked model responses, ran requested functions, supplied service dependencies, requested corrections, recorded execution steps, and converted messages for each provider.
None of that work added features that set our product apart.
We wanted to know whether Pydantic AI could reduce that supporting code while keeping execution steps visible when something went wrong. We were not looking for a no-code agent builder. We wanted a Python framework that used ordinary types to describe the structure of an agent's data. It also needed to expose tools limited to specific tasks, supply services or data for each request, and return an output object checked against the expected structure.
We tested how Pydantic AI handles model calls and checks their results. Its Agent manages model interactions and uses Python types to define the expected data structure. Tools are application functions that the model can ask to run. Pydantic AI checks their inputs before execution and checks the final result against the expected output type. Dependency injection supplies the services or data an agent needs rather than making it create them itself. Pydantic AI provides this through RunContext. It also supports retries and observability, which means recording execution details so engineers can investigate failures.
We verified those features against the official Pydantic AI documentation and inspected the implementation and release history in the Pydantic AI repository. We reviewed Codify's year-in-production retrospective and recorded its 12,000-plus active-user footprint as external adoption context, not as a deployment we operated. We treated that number as an adoption signal, not as evidence that the framework would fit our workload.
Our main comparison target was LangGraph. A graph-based workflow represents tasks as connected steps, with rules that determine which step runs next. We already use this approach when work must resume after interruptions, wait for human approval, or preserve progress across restarts. Here, we asked a narrower question. Did a Python service with several tools and one structured result need that explicit workflow structure?
To answer that, we built a 160-scenario tool-call matrix and ran each scenario through Pydantic AI and LangGraph adapters. For a local reproduction, we would cover valid tool calls, malformed or incomplete arguments, tool failures and retries, business-rule rejections, and multi-step calls with request-scoped dependency state. The locked evidence establishes 160 scenarios overall, but not the counts in these categories.
We replayed fixed model messages rather than asking a live model to improvise every run. That choice let us compare how each framework checked data, sent calls to tools, handled retries, and managed information across steps. We were not comparing which provider happened to generate better arguments that afternoon.
These tests measured correct behavior and the effort needed to operate each implementation. They did not rank response times. Replaying fixed messages did not reproduce real model delays, changing network conditions, or providers slowing requests when usage exceeded their limits.
Hands-On Walkthrough: Setup, Execution & Output
We started with a clean Python virtual environment and installed the published package directly:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install pydantic-ai
pip freeze > requirements.lock
For a basic check against the live service, we supplied a provider key through an environment variable rather than embedding it in code:
export OPENAI_API_KEY="replace-with-a-test-project-key"
We gave that key access only to a test project with a spending limit. We also kept model requests disabled in unit tests and reserved live calls for integration tests. That separation mattered because a retrying agent can consume more calls than a conventional one-request endpoint.
Our smallest test modeled an inventory decision that a production service would handle. The agent could inspect stock but could not mutate it. The final reservation remained an application-side operation after validation and authorization.
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, ModelRetry, RunContext
@dataclass
class Dependencies:
inventory: dict[str, int]
tenant_id: str
class ReservationDecision(BaseModel):
action: str = Field(pattern="^(reserve|reject)$")
sku: str
quantity: int = Field(gt=0, le=10)
reason: str
agent = Agent(
"openai:gpt-4.1-mini",
deps_type=Dependencies,
output_type=ReservationDecision,
retries=2,
instructions=(
"Evaluate inventory requests. Always call check_stock before deciding. "
"Reserve only when the requested quantity is available."
),
)
@agent.tool(retries=2)
def check_stock(
ctx: RunContext[Dependencies],
sku: str,
quantity: int,
) -> dict[str, object]:
"""Return tenant-scoped stock availability for a SKU."""
if quantity <= 0:
raise ModelRetry("Quantity must be greater than zero.")
available = ctx.deps.inventory.get(sku, 0)
return {
"tenant_id": ctx.deps.tenant_id,
"sku": sku,
"requested": quantity,
"available": available,
"can_reserve": available >= quantity,
}
if __name__ == "__main__":
dependencies = Dependencies(
inventory={"GPU-A10": 4, "GPU-L40S": 0},
tenant_id="tenant-demo",
)
result = agent.run_sync(
"Can we reserve two GPU-A10 units?",
deps=dependencies,
)
print(result.output.model_dump_json(indent=2))
A representative successful run produced:
{
"action": "reserve",
"sku": "GPU-A10",
"quantity": 2,
"reason": "Four units are available, so the request for two units can be fulfilled."
}
The important result was not the wording. We received a ReservationDecision, not a string that our application had to parse optimistically.
We confirmed three boundaries in this example:
- Pydantic validated the tool-call arguments before
check_stockused them. -
RunContextsupplied each customer's inventory data separately, rather than storing it in variables shared across the application. - The final model response had to satisfy
ReservationDecisionbefore our application accepted it.
We then replaced the live provider with test fixtures containing fixed model responses for the 160-scenario harness. Each framework adapter received equivalent tool schemas, which describe each tool's expected inputs. Both received equivalent dependency data and the same sequence of model messages. Our assertions checked whether invalid arguments reached the tool body, whether retry state remained visible, whether final outputs were typed, and how much explicit routing code each implementation required.
Pydantic AI removed most of our schema-conversion and final-output parsing code. LangGraph gave us a more explicit representation of transitions. Neither result surprised us, but the trade-off became clear. Pydantic AI simplified agents with defined input and output types. LangGraph made workflow steps and branches easier to inspect.
For teams building similar evaluation harnesses, we keep additional implementation patterns in our AI tools collection.
Pydantic AI's Limitations and Production Risks
The first limitation was conceptual: checking data types did not guarantee a correct model decision.
Pydantic AI reliably validated data after the model produced it. It could not guarantee that the model chose the correct tool, selected the correct record, or made a sound business decision. A stock keeping unit, or SKU, identifies an inventory item. A valid ReservationDecision could still identify the wrong item if the prompt or retrieved information was wrong.
We therefore kept permission checks, inventory changes, payment, and irreversible changes outside the agent. The model proposed an action; application code following fixed rules decided whether to execute it.
Our second problem was retries multiplying the number of model calls and tool executions. When malformed arguments triggered validation or a tool raised ModelRetry, the framework could ask the model to repair the call. That was convenient for read-only tools. It became dangerous for tools with side effects.
During our failure scenarios, we deliberately injected a timeout after a simulated write. From the agent's perspective, the tool had failed. From the downstream system's perspective, the write had already happened. Idempotency means that repeating an operation has no additional effect; without it, retrying could apply the same change twice.
We addressed that in three ways:
- Our application assigned each data-changing operation a unique key so the receiving service could recognize repeat attempts.
- We limited tool retries and classified failures to decide which ones warranted another attempt.
- When a write's outcome was unclear, we checked whether it had completed instead of automatically running it again.
We also found that broad exception handling made retry behavior harder to reason about. We stopped converting every exception into ModelRetry. Schema or recoverable input problems could go back to the model; authentication failures, permission failures, and downstream outages terminated the run or entered our service-level retry queue.
Pydantic AI made it easy to supply services and data for each request, but we still had to manage those resources. Passing a database connection through RunContext did not determine when to reuse connections, commit changes, cancel work, or release resources. We supplied service objects that reused connections managed by the application, rather than creating a new client for every tool call.
When we moved an older experiment into the current test suite, we found that the framework's programming interface had changed. The older code used naming and examples from an earlier result API, while the installed package expected the current output-oriented interface. We made installations repeatable by fixing the package version and saving the exact dependency versions in a file under version control. We also added a framework-upgrade test that imports every agent, generates its schemas, and executes one fixture before dependency updates can merge.
Giving an agent too many tools created another practical problem. Similar tools increased the amount of tool-definition data we sent to the model and made its choices less predictable. We obtained better behavior by presenting a narrow tool set for each agent rather than registering every internal service method. We also renamed ambiguous parameters and wrote descriptions around business meaning, not implementation details.
Observability required deliberate data handling. We could trace model calls, tool executions, retries, and validation failures, but raw prompts and tool results sometimes contained customer identifiers. Before collecting execution records centrally, we added filtering to remove sensitive data as those records were captured. We also kept development records separate from records retained for production. Observability was useful, but collecting traces without filtering sensitive data would have created a data-governance problem.
Finally, Pydantic AI did not eliminate the need to design our deployment architecture. Under concurrent load, we still needed:
- Time limits that also stop related work when a request ends
- Controls on request rates and longer waits between retries when providers are busy
- Limits on how many tasks run at once
- Queues for long-running jobs
- Protection against repeated changes when operations retry
- Saved progress so interrupted work can resume
- Separate spending limits and model permissions for each customer
- Linked execution records across web requests, model calls, and tool calls
A container running an Agent was easy to start. Operating the full service still required coordinating multiple systems.
What This Article Could Not Verify
Our replay tests did not measure production model latency, network variation, or provider throttling.
Scale, Latency & Cost vs. Alternatives
Our replay tests exercised validation, tool dispatch, and retry behavior, not production latency. We would measure remote model calls, external tools, and retry round trips separately before drawing conclusions about which component dominates production delay. We therefore did not assign a universal latency advantage to either Pydantic AI or LangGraph.
A provider SDK, or software development kit, is a code library for calling the provider's service. We compared the options this way:
| Criterion | Pydantic AI | LangGraph | Direct provider SDK | Managed cloud agent |
|---|---|---|---|---|
| Best fit in our tests | Typed Python agents with a compact control loop | Stateful, branching, resumable workflows | Small integrations with minimal abstraction | Teams outsourcing more runtime infrastructure |
| Structured output | Native Pydantic-oriented workflow | Available, but we wired it into graph state and nodes | Provider-specific or application-managed | Platform-specific |
| Tool argument validation | Integrated before tool execution | Explicit in our nodes or wrappers | Usually required application code | Usually integrated but less portable |
| Dependency injection | Direct through typed run context | Passed through graph state or runtime configuration | Entirely application-defined | Platform-specific |
| Retry handling | Compact and convenient, but easy to overuse | More verbose and more visibly routed | Fully manual | Configurable within platform limits |
| Workflow visibility | Good for agent runs, less explicit for complex branching | Strong; nodes and edges expose transitions | Depends on custom instrumentation | Strongest inside the vendor console |
| Durable long-running execution | Requires additional architecture or integration | Better fit for workflows that save progress so they can resume | Fully custom | Often built in |
| Portability | High at the Python application layer | High, with framework-specific graph definitions | Highest code-level control, lowest abstraction | Lowest |
| Framework license cost | No framework fee in our setup | No framework fee in our setup | No framework fee | Usage and platform charges |
| Main operational risk we observed | Treating validation as business correctness | Graph complexity exceeding workflow complexity | Rebuilding validation and tracing inconsistently | Lock-in and constrained runtime behavior |
For our fixture matrix, Pydantic AI made malformed input handling and final-output validation more compact. LangGraph made failure branches and multi-step state transitions easier to inspect as explicit workflow structure. Both could implement the full matrix; the engineering difference was where the complexity lived.
We estimated development costs and how long maintenance savings would take to cover migration work. Framework choice did not change the price of tokens, the units providers use to measure model input and output. As a hypothetical budgeting example—not a measured implementation result—we can assume 32 engineering hours with Pydantic AI versus 48 hours with LangGraph. Our estimated engineering cost, including salary and associated expenses, was $150 per hour. The initial difference was:
(48 hours - 32 hours) × $150/hour = $2,400
For a migration, we estimated 40 hours to replace an established orchestration layer, including regression tests and deployment work. If typed validation and simpler debugging saved four engineering hours per month, the labor-only break-even point was:
40 migration hours ÷ 4 hours saved per month = 10 months
All figures in this budgeting example are hypothetical planning inputs, not findings from our implementation or the 160-scenario benchmark. Teams should substitute their own labor rate, migration size, incident frequency, and maintenance burden.
Model spending did not automatically decrease. The same provider, prompt, and tool loop cost roughly the same regardless of framework. Poorly bounded retries could make Pydantic AI more expensive than a single-pass direct SDK call. Conversely, catching invalid output before it entered downstream systems reduced the much larger cost of debugging corrupted state.
For teams deciding between a compact typed agent and a durable graph architecture, our AI engineering services cover workload-specific design and deployment reviews.
Our Final Verdict: When to Deploy, When to Skip
We would deploy Pydantic AI for a Python service when the main workflow is an agent loop with typed tools and a structured final result. It separated variable model responses from application checks that consistently enforce defined rules, without forcing every interaction into a graph.
We would not use it as evidence that an agent is correct, secure, or operationally complete. Its types protected data boundaries. They did not replace authorization, idempotency, evaluation, or workflow persistence.
Deploy this if:
- We already use Pydantic across the service.
- We need typed tool arguments and validated final outputs.
- The workflow is mostly request-response or a bounded tool loop.
- We want provider flexibility without writing adapters for every call.
- We can keep irreversible side effects behind deterministic service code.
- We are prepared to pin versions and run agent-level regression fixtures.
- We will instrument retries, tool failures, token usage, and validation errors.
We would hold off or avoid Pydantic AI when these conditions apply:
- Our main workflows run for long periods, save progress for later resumption, and pause for human review or approval.
- Our process has many explicit branches that stakeholders must inspect visually.
- We expect the framework to provide queues, schedulers, durable execution, or rate-limit governance by itself.
- Our team is not predominantly Python.
- We cannot make mutating tools idempotent.
- We need a hosted service to configure and operate agents more than a framework that checks application data types.
- A direct provider call already solves the complete use case with little parsing or orchestration.
For a compact Python agent service, we would start with Pydantic AI before reaching for LangGraph. Our implementation needed less code to check data and supply the agent with services or data. Once the workflow became a durable process with explicit transitions, recovery points, and operator intervention, we preferred the graph model.
The wrong decision is not choosing one framework over the other. The wrong decision is deploying an agent loop as if validated JSON were equivalent to reliable business execution.
We would use Pydantic AI as a typed boundary inside a production architecture, not as the architecture itself. Teams with a specific deployment or migration question can contact us for an architecture review.
Top comments (0)