PydanticAI vs. LangChain: A Tale of Two AI Frameworks
Exploring the strengths, differences, and trade-offs between two approaches to building conversational Gen AI agents.
Overview
If you’ve been keeping up with the world of AI tooling, you’ve likely encountered a variety of frameworks aiming to simplify the creation of conversational agents. One standout is LangChain, a well-established library that makes it easy to chain together various operations for language models. It’s packed with features and pre-built components, making it a go-to for rapid prototyping and development.
Recently, however, a new contender has entered the scene: PydanticAI. Built on the foundation of Pydantic, this library promises to make it less painful to build production grade applications with Generative AI, incorporating type-safety features and a schema-first alternative to more traditional approaches.
To dive deeper, we re-implemented a conversational agent originally written with LangChain, this time using PydanticAI. We based our implementation on a fantastic example from Nir Diamant’s Gen-AI Agents repository, which is an excellent resource for anyone looking to explore generative AI in action. Nir’s notebooks are filled with easy-to-follow, practical examples—making them the perfect way to experiment with different techniques and kick off your project.
This exercise wasn’t just about contributing to Nir’s repository (though we’re excited to add the first PydanticAI example to it!). It was also an opportunity to evaluate how PydanticAI’s schema-driven approach stacks up against LangChain’s component-based model. In this post, we’ll dive into concrete examples, compare the two frameworks’ approaches, and help you decide which one might suit your needs.
The LangChain Setup
Nir’s original LangChain-based notebook (simplified here) demonstrates how to create a conversational agent that uses memory to maintain context1 over multiple interactions. Here’s what it looks like:
# Initialize the language model
llm = ChatOpenAI(model="gpt-4o-mini", max_tokens=1000, temperature=0)
# Create a simple in-memory store for chat histories
store = {}
def get_chat_history(session_id: str):
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# Create the prompt template
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful AI assistant."),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])
# Combine the prompt and model into a runnable chain
chain = prompt | llm
# Wrap the chain with message history
chain_with_history = RunnableWithMessageHistory(
chain,
get_chat_history,
input_messages_key="input",
history_messages_key="history"
)
What’s Happening Here?
LangChain simplifies the task by providing ready-made components for:
• Language Models (LLM): ChatOpenAI
connects to the model.
• Memory Systems: ChatMessageHistory
keeps track of the conversation.
• Prompt Templates: ChatPromptTemplate
defines how the input is formatted.
These components are then stitched together with RunnableWithMessageHistory
, which handles the logic for injecting and retrieving historical messages. This makes LangChain both powerful and beginner-friendly.
Enter PydanticAI: A Schema-First Approach
Now let’s try to recreate the same conversational agent using PydanticAI. Below is a simplified version of the code found in this new notebook implementation:
# Initialize the language model
agent = Agent(
model='openai:gpt-4o-mini',
system_prompt='You are a helpful AI assistant.',
)
store: dict[str, list[bytes]] = defaultdict(list)
def ask_with_history(user_message: str, user_session_id: str):
"""Asks the chatbot the user's question and stores the new messages in the chat history."""
# Get existing history to send to model
chat_history = list(chain.from_iterable(
MessagesTypeAdapter.validate_json(msg_group)
for msg_group in store[user_session_id]
))
# Ask user's question and send chat history.
chat_response = agent.run_sync(user_message, message_history=chat_history)
# Store new messages in chat history.
store[user_session_id].append(chat_response.new_messages_json())
return chat_response
As you can see, even in these relatively small and simple examples, there are quite a few differences between the two versions of the code. Including differences in the way validation is performed, the API that each library exposes, the flexibility of each library and more.

Let’s break them down:
Data Handling and Validation
LangChain:
In the LangChain snippet, we see:
A store dictionary that maps
session_id
strings toChatMessageHistory
objects, which act as an in-memory record of previous messages.The prompt and chain setup rely on templates and placeholders, such as
MessagesPlaceholder
, to dynamically inject elements like conversation history.
There’s no explicit schema enforcement. The correctness of the data format (e.g., ensuring that history is a list of messages) is largely based on conventions and the internal logic of LangChain components.
PydanticAI:
The PydanticAI snippet uses
MessagesTypeAdapter.validate_json
to ensure that any retrieved message history is valid JSON and conforms to a known data structure. This explicit validation guarantees that the data passed to the agent is always in the correct format.By integrating a type adapter and leveraging Pydantic’s validation mechanisms, we add a layer of assurance that any historical messages and user inputs are well-formed before they reach the model. reduces the likelihood of subtle errors caused by malformed or unexpected data.
Conversation Structure and Composition
LangChain:
The LangChain example chains together a ChatOpenAI
model and a ChatPromptTemplate
using a pipeline-like syntax: prompt | llm
. This chain is then wrapped with RunnableWithMessageHistory
, which manages stateful conversation by injecting and retrieving the chat history dynamically.
This approach is compositional and flexible. Developers can stitch components together without needing to define strict input/output contracts. While this flexibility is powerful, it can make the structure of the code more implicit and dependent on conventions.
PydanticAI:
With PydanticAI, we start by defining an Agent with a specified system_prompt
. Instead of chaining multiple components explicitly, we pass in typed, validated message_history
and user inputs directly. The agent.run_sync()
method expects these well-defined inputs, and its outputs are structured and ready to be cleanly stored or processed further.
The code reflects a more controlled and predictable data flow. By the time we call agent.run_sync()
, the message history has already been validated, ensuring consistency. This approach integrates validation and conversation logic into a single step, eliminating the need to layer multiple components that rely on convention.
Session and History Management
LangChain:
History is handled via an in-memory store and LangChain’s internal mechanisms. This setup is straightforward but can feel somewhat “magical,” as the framework implicitly manages parts of the process. This dual handling—where history is partially managed by our code and partially by LangChain—can result in a lack of clarity about where responsibility lies.
PydanticAI:
In PydanticAI, the responsibility for storing and retrieving the history is entirely in the developer’s hands. While this requires writing a bit more code, it offers greater transparency. There’s no hidden logic managing state behind the scenes, making it easier to understand and control how history is handled.
Extensibility and Support
LangChain:
While not immediately obvious from the code examples, LangChain’s mature ecosystem supports a wide range of LLMs, vector databases, and tools out of the box. Features like PostgresSaver
or LangGraph
make it easier to build complex systems without needing to reinvent the wheel. This extensive integration capability allows developers to quickly prototype and scale their applications with minimal friction.
PydanticAI:
PydanticAI, being a newer library, offers fewer built-in integrations. While it provides great flexibility, you may need to write more glue code, such as implementing custom database storage for conversation history. For projects with complex requirements or dependencies, this additional effort can increase development time but also gives you more control over the implementation details.
Conclusion
Both LangChain and PydanticAI have their strengths and trade-offs:
LangChain takes a flexible, component-based approach that allows rapid assembly of chains, prompts, and memory. It’s easy to get started, but data formats and contracts are more implicit. In more complicated projects where you have to combine different constructs all into a single system, you may find yourself juggling numerous parts and configurations.
PydanticAI prioritizes explicit schemas and validation. Inputs and outputs must conform to defined models before interacting with the agent. It provides less prebuilt constructs to use, meaning you’ll have to write more glue code yourself. While this may require a bit more upfront effort, it can make the system more robust, predictable, and easier to maintain in the long run. That said, being a new library, the community is still small and at the end of the day PydanticAI still needs to prove itself.
At the end of the day, both LangChain and PydanticAI can cater to teams of various sizes and experience levels, making the decision highly context-dependent. LangChain is an established, dominant player with a rich ecosystem and a large community, which makes it an obvious choice for many projects. However, it can sometimes feel verbose and complex to write in.
PydanticAI, on the other hand, promises to reduce some of this friction with its schema-driven approach, offering a cleaner and more controlled experience. But as a relatively new entrant, its longevity and ecosystem maturity remain to be seen. For now, we see PydanticAI as a compelling option for teams that value explicit validation and are willing to experiment with a newer framework. Ultimately, the choice between these frameworks depends on your specific priorities: ecosystem richness and flexibility versus strict data handling and clarity.
This post only scratches the surface of what LangChain and PydanticAI can do. We haven’t even touched on advanced features like structured responses in PydanticAI, tools for debugging (e.g., LangSmith vs. Logfire), or strategies for handling multi-phase agents. If you’re interested in a deeper dive into these topics or more advanced use cases, let us know—we’d love to explore these in future posts!
If you’re curious to explore these examples further, check out Nir Diamant’s Gen-AI Agents repository, where you’ll find these implementations and more.
Finally, if you need help with your own AI or Data Engineering needs, reach out to us at contact@hipposys.com. We’d love to talk with you!
In conversational agents, “memory” refers to the ability to maintain context across multiple interactions. Importantly, LLMs don’t have real memory—they don’t store or recall past conversations. Instead, memory is simulated by sending the entire conversation history back to the model as part of the prompt for each new message. This allows the agent to respond in a way that feels context-aware, even though it’s essentially reprocessing the entire history every time.