Search Here

Strings are Dead: Why PydanticAI is the Only Way I Write Agents Now

Home / Strings are Dead: Why PydanticAI is the...

Strings are Dead: Why PydanticAI is the Only Way I Write Agents Now Strings are Dead: Why PydanticAI is the Only Way I Write Agents Now Strings are Dead: Why PydanticAI is the Only Way I Write Agents Now

Strings are Dead: Why PydanticAI is the Only Way I Write Agents Now

Spread the love

We need to have a serious talk about how we’re building agents in late 2025.

If your production codebase still looks like response = client.chat.completions.create(…) followed by a prayer and a try-except block around json.loads(), you are choosing violence.

For the last two years, we’ve treated LLMs like chatty interns—asking them nicely to “please output JSON,” threatening them with “do not hallucinate” in all-caps, and hoping they don’t add a helpful “Here is your JSON:” preamble that crashes the entire backend.

But the industry has shifted. We aren’t building chatbots anymore; we’re building headless intelligence. And headless systems don’t speak English. They speak Types.

I’ve completely stopped writing “string-based” agents. No more regex parsing. No more retry loops for malformed JSON. I’ve moved exclusively to PydanticAI. Here is why, and why you probably should too.

The “String Parsing” Hell (AKA The Old Way)

Let’s be real. We’ve all written this code. You write a perfect prompt, deploy it, and at 3 AM your PagerDuty goes off because Llama 3.3 decided to format a list with bullets instead of brackets.

Your code looks like this spaghetti:

Python

# The “Hope and Pray” Pattern (Don’t do this)

response = client.chat.completions.create(

    model=”gpt-4o”,

    messages=[

        {“role”: “system”, “content”: “You are a data extractor. Output JSON only.”},

        {“role”: “user”, “content”: “Extract info for Ayaz, 28 years old.”}

    ]

)

content = response.choices[0].message.content

# 🚩 DANGER ZONE

try:

    # If the LLM says “Sure! { … }” this crashes

    data = json.loads(content) 

except json.JSONDecodeError:

    # Now you’re writing regex to clean string output?

    # Stop it. Get some help.

    data = fallback_regex_cleaner(content)

This isn’t engineering. This is guessing. You are building a house on a foundation of sand, where the sand is a stochastic model that occasionally decides to speak French or markdown.

In 2023, this was acceptable because we didn’t know better. In 2025, it’s negligence.

Also Read:AI Browsers Are Becoming Development Tools

Level 1: The “Type-Safe” Pivot

The Pydantic team (the legends who made FastAPI the standard) finally dropped PydanticAI, and it brings that exact same “it just works” energy to agents.

The core philosophy is simple: Strings are for humans. Types are for agents.

Instead of parsing strings, you define Models. You don’t ask the LLM for JSON; you constrain the LLM’s brain to a schema. If the model tries to output something that doesn’t fit your type definition, the framework automatically catches it, feeds the validation error back to the LLM, and forces it to self-correct—all before you even see the result.

Here is what that same workflow looks like now:

Python

from pydantic import BaseModel, Field

from pydantic_ai import Agent

# 1. Define the Schema (The Contract)

class UserExtraction(BaseModel):

    name: str

    age: int = Field(…, ge=18, description=”User must be an adult”)

    skills: list[str]

    confidence_score: float

# 2. Define the Agent

# Notice we pass the type directly to the agent. 

agent = Agent(

    ‘openai:gpt-4o’,

    result_type=UserExtraction,  # <— The Magic

    system_prompt=”You are an expert HR data extractor.”

)

# 3. Run it (Type-Safe Execution)

# The result is NOT a string. It’s a UserExtraction object.

result = agent.run_sync(“My name is Ayaz, I’m 28, and I code in Python.”)

print(result.data.name)   # Output: ‘Ayaz’

print(result.data.age)    # Output: 28 (int, not string)

print(type(result.data))  # <class ‘UserExtraction’>

If the LLM tries to return age: “twenty-eight”, Pydantic raises a validation error. PydanticAI catches that error, sends it back to the LLM saying “Field ‘age’ must be an integer, you sent a string”, and the LLM fixes it. You get guaranteed reliability without writing a single line of parsing logic.

Also Read:Vibe Checks Aren’t Enough: How to Actually “Unit Test” Your AI Agents

Level 2: The End of Global Variables (Dependency Injection)

This is where the “Script Kiddies” get separated from the “Software Engineers.”

In the old days (looking at you, LangChain 0.1), if you wanted to give an agent access to a database, you had to deal with global variables or messy partial functions to pass your DB connection into a tool. It was a testing nightmare.

PydanticAI introduces a Dependency Injection system (RunContext) that is cleaner than anything else I’ve seen.

You define a Dataclass for your dependencies (DB connections, API keys, User IDs), and you inject them at runtime.

Python

from dataclasses import dataclass

from pydantic_ai import RunContext

@dataclass

class MyDeps:

    db_connection: object

    user_id: int

# Define the agent with the dependency type

agent = Agent(

    ‘openai:gpt-4o’,

    deps_type=MyDeps, 

    system_prompt=”You are a helpful banking assistant.”

)

# Define a tool that uses the dependencies

@agent.tool

def get_balance(ctx: RunContext[MyDeps]) -> str:

    # We access the DB safely through the context

    balance = ctx.deps.db_connection.fetch_balance(ctx.deps.user_id)

    return f”${balance:.2f}”

# Run it

my_db = Database() # mock db

deps = MyDeps(db_connection=my_db, user_id=42)

result = agent.run_sync(“How much money do I have?”, deps=deps)

Why is this huge? Testing.

You can now easily swap out MyDeps with a mock database for your unit tests. You can run the exact same agent in production with a real DB and in CI/CD with a fake one, without changing a single line of agent code.

Also Read:How AI Agents Turn Prompts into Full Workflows

Level 3: Testing Without Burning Cash

One of the biggest blockers for AI engineering is: “How do I test this without spending $50 on API credits every time I run pytest?”

PydanticAI has a built-in TestModel. It’s a mock model that acts exactly like an LLM but costs nothing and returns deterministic data based on your function tools.

Python

from pydantic_ai.models.test import TestModel

# Override the model for testing

def test_agent_logic():

    agent.override(model=TestModel())

    # This won’t hit OpenAI. It will simulate a tool call.

    result = agent.run_sync(“Get my balance”, deps=test_deps)

    assert result.data == “$100.00”

This means you can write unit tests for your logic (did the agent call the right tool? did it handle the error?) without waiting for network latency or paying OpenAI.

Real-World Build: A “Headless” Triage Agent

Let’s put it all together. Imagine you want an agent that reads customer support tickets and decides:

  1. Is this a billing issue or a technical issue?
  2. How urgent is it?
  3. Draft a JSON object to send to our ticketing API.

This is a classic “Router” pattern, but typed.

Python

from typing import Literal

from pydantic import BaseModel

from pydantic_ai import Agent

class TicketRouting(BaseModel):

    category: Literal[‘Billing’, ‘Technical’, ‘General’]

    priority: Literal[‘Low’, ‘High’, ‘Critical’]

    summary: str

    suggested_action: str

agent = Agent(

    ‘openai:gpt-4o’,

    result_type=TicketRouting,

    system_prompt=”You are a triage AI. Categorize tickets ruthlessly.”

)

ticket_text = “HELP! My server is on fire and I can’t login! I’m losing money!”

result = agent.run_sync(ticket_text)

# We can immediately use this in our code

if result.data.priority == ‘Critical’:

    pagerduty.trigger(result.data.summary)

elif result.data.category == ‘Billing’:

    finance_team.notify(result.data)

Notice how clean the downstream code is? if result.data.priority == ‘Critical’. No string parsing. No generic “contains” checks. It is safe, compiled, and ready for production.

Streaming: The “It Feels Fast” Factor

Here is the cherry on top. If you are building a UI, you don’t want the user staring at a spinner while the agent thinks. You want Streaming.

PydanticAI supports run_stream. But unlike other libraries that just stream raw text, PydanticAI streams partial structures.

Python

async with agent.run_stream(ticket_text) as result:

    async for message in result.stream():

        print(message) 

        # Prints validated partial chunks as they arrive

This allows you to update your frontend in real-time while still maintaining type safety on the final result.

The Verdict: PydanticAI vs. LangGraph

I get asked this on LinkedIn every day: “Ayaz, should I use LangGraph or PydanticAI?”

Here is the honest answer for late 2025:

FeatureLangGraphPydanticAI
Best ForComplex, multi-step state machines with loops and branching.“Headless” workers, data extraction, and reliable tool calling.
Mental ModelNodes and Edges (Graph Theory).Functions and Types (Software Engineering).
ComplexityHigh. Lots of boilerplate.Low. Feels like writing FastAPI.
ReliabilityGood, but you handle state manually.Incredible. Types ensure correctness.

My Rule of Thumb:

  • If I am building a chatbot that needs to remember conversation history and jump between different “personalities,” I use LangGraph.
  • If I am building a system that needs to take an invoice, extract data, update a database, and send an email (reliably, 1000 times a day), I use PydanticAI.

Conclusion

We are moving past the “Wow, it talks!” phase of AI. We are in the “Make it work, every single time” phase.

Prompt Engineering is becoming Context Engineering. And writing scripts is becoming Software Architecture. PydanticAI is the bridge that takes us there.

If you are still wrestling with regex to parse your agent’s output, stop. Install pydantic-ai, define your types, and let the framework handle the mess.

Strings are dead. Long live Types.