LangGraph vs Pydantic AI: Which Agent Framework Should You Use in 2026?
Use Pydantic AI when your agent is mostly linear — take input, call a few tools, return validated structured output — and you want to move fast with type safety. Use LangGraph when the work is a genuine state machine that must pause for humans, run parallel branches, or survive restarts. I built the *same* triage agent in both: Pydantic AI took 22 lines, LangGraph took 49 — but LangGraph gave me durable checkpoints for free. Most teams should start on Pydantic AI and graduate to LangGraph only when the workflow shape demands it.
Every "which AI agent framework should I use" post I could find compares feature checklists copied from the docs. I wanted numbers. So I installed both LangGraph 1.2.10 and Pydantic AI 2.23.0 on Python 3.14, built the exact same agent in each — a support-ticket triage bot that calls a tool and returns structured output — and ran both with their offline test models so no API key or network call was involved. Everything below is measured on my machine, not paraphrased from a changelog.
The short version: these two frameworks are aimed at different problems, and once you see the same task written both ways the trade-off is obvious. Pydantic AI optimizes for *getting a validated answer out of a model with the least ceremony.* LangGraph optimizes for *orchestrating a durable, inspectable workflow.* Here's the head-to-head.
The one-sentence answer (before the details)
If you only remember one thing: Pydantic AI is a great default; LangGraph is what you graduate to. Reach for Pydantic AI when the agent is basically "prompt in, tools, structured result out." Reach for LangGraph when your workflow is a real state machine — it pauses for human approval, recovers from a crashed server mid-run, fans out into parallel branches, or needs an audit trail of every step.
| If you need… | Pick |
|---|---|
| Type-safe, validated output with minimal code | Pydantic AI |
| A mostly-linear "call model, call tools, return" agent | Pydantic AI |
| Fast prototyping, small dependency footprint | Pydantic AI |
| Long-running workflows that survive restarts | LangGraph |
| Human-in-the-loop pauses / approvals | LangGraph |
| Parallel branches, loops, explicit state | LangGraph |
| An audit log of every decision (compliance) | LangGraph |
Both are Python-first, both are actively maintained (LangGraph hit v1.0 in late 2025 and is now on the 1.2.x line; Pydantic AI shipped its v2 in June 2026), and — worth saying up front — they aren't mutually exclusive. Plenty of teams run a Pydantic AI agent *as a node inside* a LangGraph graph. But when you're choosing the primary framework, the decision is about workflow shape.
The test: the same triage agent, written both ways
To make the comparison fair I picked a task both frameworks should handle well: a support-ticket triage agent. It takes a ticket, calls one tool (look up the customer's paying tier), and returns a structured result — a category, a 1–5 priority, and a needs_human flag. Here it is in Pydantic AI:
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.test import TestModel
class Triage(BaseModel):
category: str = Field(description="billing | technical | account | other")
priority: int = Field(ge=1, le=5)
needs_human: bool
@dataclass
class Deps:
customer_id: str
agent = Agent(
TestModel(), # in prod: 'openai:gpt-4o' / 'anthropic:...'
deps_type=Deps,
output_type=Triage, # <- this is the whole "structured output" story
system_prompt="Triage the support ticket. Use tools when useful.",
)
@agent.tool
def customer_tier(ctx: RunContext[Deps]) -> str:
"""Look up the paying tier of the current customer."""
return {"c_1": "enterprise", "c_2": "free"}.get(ctx.deps.customer_id, "unknown")
result = agent.run_sync("I was charged twice!", deps=Deps(customer_id="c_1"))
print(type(result.output).__name__, result.output)Run it and the output is already a validated object — not a dict, not a string you have to parse:
output type : Triage
validated : category='a' priority=1 needs_human=False
is Triage : True(The values are dummies because TestModel doesn't run a real LLM — but note priority=1 still satisfies the ge=1, le=5 constraint. The framework guarantees the shape.) You declare output_type=Triage and Pydantic AI handles the tool-call round-trip to coerce the model's output into a validated Triage for you. That's the entire mechanism.
Now the same agent in LangGraph. Because LangGraph is a graph framework, you describe the workflow as nodes and edges — a model node, a tool node, and the routing between them:
class State(TypedDict):
messages: Annotated[list, add_messages]
customer_id: str
result: Triage | None
def call_model(state):
return {"messages": [model.invoke(state["messages"])]}
def run_tool(state):
call = state["messages"][-1].tool_calls[0]
out = customer_tier(**call["args"])
return {"messages": [ToolMessage(content=out, tool_call_id=call["id"])]}
def finalize(state):
# LangGraph does NOT validate for you — you parse the text yourself.
return {"result": Triage(**json.loads(state["messages"][-1].content))}
def route(state):
return "tools" if state["messages"][-1].tool_calls else "final"
g = StateGraph(State)
g.add_node("model", call_model)
g.add_node("tools", run_tool)
g.add_node("final", finalize)
g.add_edge(START, "model")
g.add_conditional_edges("model", route, {"tools": "tools", "final": "final"})
g.add_edge("tools", "model")
g.add_edge("final", END)
app = g.compile(checkpointer=MemorySaver()) # <- durability, for freeNotice the difference in *shape*, not just length. In Pydantic AI you declare what you want (output_type=Triage) and the framework figures out the control flow. In LangGraph you draw the control flow yourself — the model→tool→model loop, the conditional edge, the finalize step. That explicitness is the point of LangGraph; it's also why it's more code.
The measurements (all from my machine)
Both agents above actually run — I executed them offline with each framework's fake/test model so no LLM was called. Here's everything I measured, on Python 3.14, fresh virtual environments:
| Metric | Pydantic AI 2.23.0 | LangGraph 1.2.10 |
|---|---|---|
| Same triage task, lines of code | 22 | 49 |
| Import statements needed | 4 | 8 |
| Structured output | automatic, validated | manual json.loads + parse |
| Cold import time (best of 3) | ~725 ms | ~830 ms |
| Durable state out of the box | opt-in (v2 capability) | built-in checkpointer |
| Lean install — packages / size | 21 pkgs / 45 MB * | 36 pkgs / 56 MB |
Default pip install <name> | 102 pkgs / 217 MB ** | 36 pkgs / 56 MB |
Two footnotes on the install numbers, because they're the most surprising and the most misread:
- \* Pydantic AI has two packages.
pip install pydantic-ai-slimgives you the core in 21 packages / 45 MB — but no model provider; you add exactly the ones you need (pydantic-ai-slim[openai],[anthropic], etc.). The slim package won't even let you *reference*'openai:gpt-4o'until you install that extra — I hit thatImportErrorin testing. That's a deliberate, honest trade-off: small by default, you opt in. - \*\* The plain `pip install pydantic-ai` is heavy. It's the batteries-included meta-package that pulls every model provider — OpenAI, Anthropic, Google, Groq, Mistral, Cohere and more — landing at 102 packages / 217 MB. If you care about footprint (serverless, containers), install
pydantic-ai-slim[your-provider], notpydantic-ai. - LangGraph's 56 MB also excludes a provider. LangGraph depends on
langchain-corebut not on any LLM client — you addlangchain-openai/langchain-anthropicseparately, same as Pydantic AI's extras. So the fair, provider-free comparison is 45 MB (Pydantic AI slim) vs 56 MB (LangGraph) — closer than the headline 217 MB number suggests.
If you take one practical thing from this article: don't pip install pydantic-ai into a Lambda or a container and wonder why the image ballooned. Use pydantic-ai-slim[openai] (or your provider) and you'll pull ~45 MB instead of ~217 MB.
Where Pydantic AI wins: structured output & type safety
This is Pydantic AI's home turf, and it shows. Because it's built by the team behind Pydantic, validation isn't a bolt-on — it *is* the framework. You define your result as a BaseModel, pass it as output_type, and every run comes back as a validated instance or raises. Constraints like priority: int = Field(ge=1, le=5) are enforced automatically.
In LangGraph, structured output is your job. In my triage graph the model returns text, and the finalize node has to json.loads it and construct the Triage itself. LangChain does offer model.with_structured_output(Triage) to push that into the model call, but it's an extra step you wire up — not the default posture of the framework. If your agent's main deliverable is *a well-shaped object your code can trust*, Pydantic AI gets you there with dramatically less glue.
If you already use Pydantic and mypy (or our online type checker) across your codebase, Pydantic AI slots in with zero new mental model — the agent's inputs, dependencies, and outputs are all just typed Python you already know how to validate.
Where LangGraph wins: durability & state you can inspect
Here's the thing that made the extra LangGraph code worth it. I compiled the graph with checkpointer=MemorySaver() and ran one ticket. Then I asked the graph how much state it had saved:
output type : Triage
validated : category='billing' priority=4 needs_human=True
is Triage : True
checkpoints : 6 saved statesSix checkpoints from a single run — one after every super-step. That's LangGraph's whole reason to exist: the state is persisted at each step, so if your process crashes between the tool call and the final answer, you resume from the last checkpoint instead of restarting the whole (paid) LLM conversation. Swap MemorySaver for a Postgres or SQLite checkpointer and that durability survives an actual server restart. This is also what powers human-in-the-loop: you can pause the graph, wait hours or days for an approval, and resume exactly where it stopped.
Pydantic AI's v2 does have a durable-execution story too — but it's opt-in and delegates to external durable engines (DBOS, Prefect, Temporal) via its capability layer, rather than shipping a built-in checkpointer. If durability is the *core* of your problem, LangGraph makes it the default; with Pydantic AI you assemble it.
One real gotcha I hit: putting a custom Pydantic type (my Triage) directly into LangGraph state triggered a msgpack serialization warning — "Deserializing unregistered type … will be blocked in a future version." Keep graph state to primitives/dicts, or register your types, and you'll avoid a future breakage.
How to actually choose
Strip away the hype and it comes down to workflow shape. Ask yourself: *is my agent a function, or a process?*
- It's a function — input goes in, a few tools fire, a structured answer comes out, done in one turn. → Pydantic AI. You'll write less code, get validated output for free, and ship faster.
- It's a process — it waits on humans, runs for minutes to days, branches in parallel, retries on failure, and you need to see every step. → LangGraph. The graph and checkpointer are exactly the machinery you'd otherwise hand-build (badly).
- You're not sure yet — start with Pydantic AI. It's the lower-commitment choice, and if you outgrow it you can wrap a Pydantic AI agent as a node inside a LangGraph graph later without throwing work away.
That last point matters: this isn't a religious war. The frameworks compose. The mistake is reaching for LangGraph's graph ceremony for what is really a single-turn structured-output task, or trying to bolt durable, resumable, human-in-the-loop state onto a framework that treats a run as one function call. Match the tool to the shape of the work.
Both frameworks are pure-enough Python that you can prototype the logic in a browser first. Paste an agent skeleton into our Online Python Compiler, stub the model, and check that your tools and types line up before you spend a token on a real LLM call.
Prototype your agent in the browser first
Sketch the tools, types, and control flow in our Online Python Compiler before you spend a token on a real model. Real CPython via WebAssembly, nothing uploaded.
Open the Online Python CompilerFree tools mentioned here
Frequently asked questions
Is Pydantic AI or LangGraph better in 2026?
Neither is universally better — they target different workflow shapes. Pydantic AI is better for mostly-linear agents that call a few tools and return validated structured output, and it needs far less code (22 lines vs 49 for the same triage agent in my test). LangGraph is better for long-running, stateful workflows that pause for humans, run parallel branches, or must survive restarts, thanks to its built-in checkpointer. Most teams should start with Pydantic AI and move to LangGraph only when the workflow genuinely becomes a state machine.
Can I use LangGraph and Pydantic AI together?
Yes. They compose well and are not mutually exclusive. A common pattern is to run a Pydantic AI agent as a single node inside a LangGraph graph — LangGraph handles the durable, multi-step orchestration while Pydantic AI handles the type-safe model call and structured output within a node. Choosing a 'primary' framework is about the overall workflow shape, not about picking one to the exclusion of the other.
Which has a smaller install, LangGraph or Pydantic AI?
On a fair, provider-free comparison, Pydantic AI slim is smaller: pydantic-ai-slim is 21 packages / 45 MB versus LangGraph's 36 packages / 56 MB (both exclude the LLM client). Watch out for the plain `pip install pydantic-ai`, though — that meta-package bundles every model provider and balloons to 102 packages / 217 MB. For lean deployments use pydantic-ai-slim[openai] (or your provider), not pydantic-ai.
Does Pydantic AI support durable execution like LangGraph?
Yes, but differently. LangGraph ships a built-in checkpointer that persists state after every step out of the box (I counted 6 saved checkpoints from a single run). Pydantic AI v2 offers durable execution as an opt-in capability that delegates to external durable engines like DBOS, Prefect, or Temporal. If durability is the core of your problem, LangGraph makes it the default; with Pydantic AI you assemble it from its capability layer.
Why is my pydantic-ai install so large?
Because `pip install pydantic-ai` installs the batteries-included meta-package that pulls every supported model provider (OpenAI, Anthropic, Google, Groq, Mistral, Cohere and more) — 102 packages, ~217 MB in my test. Install `pydantic-ai-slim` plus only the provider extra you need (e.g. pydantic-ai-slim[openai]) to get the ~45 MB core instead. Note the slim package won't let you reference a provider's model string until that extra is installed.
Which framework is easier to learn?
Pydantic AI is easier to start with, especially if you already know Pydantic. You declare an output type and a few tools and you have a working, type-safe agent — the same task took 22 lines and 4 imports in my test. LangGraph asks you to think in nodes, edges, state, and reducers up front (49 lines, 8 imports for the identical agent). That graph model is more to learn, but it's exactly what you need once your workflow stops being a single function call.
Do LangGraph and Pydantic AI work on Python 3.14?
Yes — I tested LangGraph 1.2.10 and Pydantic AI 2.23.0 on Python 3.14.2 and both installed and ran cleanly. One minor note: putting a custom Pydantic model directly into LangGraph's graph state produced a msgpack serialization warning about unregistered types, which is worth avoiding by keeping state to primitives/dicts or registering your types.