TypedDict, Dataclass, Pydantic - three ways to define the "data contract" of the graph.
From type hints to runtime verification, choosing the appropriate state pattern is the first step in building a robust Agent.
In Module 1, you have learned the core skeleton of LangGraph:State, Node, Edge, StateGraph. you know how to useTypedDictDefine a simple State and let data be passed between nodes through it.
But Module 1 is just the tip of the iceberg. Agents in the real world need to handle more complex state structures: conversation contexts containing message history, business fields that need to be verified, optional attributes with default values... These all require you to have a deeper understanding of State Schema.
State (state definition): How to use different Python data types to define the state pattern of graphs, including TypedDict, Dataclass, Pydantic; and how to use reducer to control state update behavior.
Memory: How to let the Agent remember the dialogue content across rounds, including In-memory checkpointer and external Memory Store.
This explanatory document focuses onLesson 1:State Schema, which is the starting point of Module 2 - figuring out how "state" can be defined in several ways, what are the advantages and disadvantages of each, and how to choose in actual projects.
State Schema is the "Data Contract" of the graph. All nodes in the entire graph rely on this contract to read and write data. Choosing the wrong Schema method will cause two typical problems:
moodThe field is set to"angry", but the type hint written is only"happy"or"sad", if there is no runtime verification, this bug will be quietly brought into the production environment.Only by understanding the three Schema definition methods can you choose the right tool in the right scenario and write LangGraph code that is maintainable and easy to debug.
When you create aStateGraph, the first step is to pass in aState Schema class:
# 这里的 MyState 就是 Schema——它定义了这张图能存放什么数据
builder = StateGraph(MyState)
This Schema does three things:
str、int、list……)Each Node in LangGraph is an independent function, and parameters cannot be passed directly between them, nor variables can be shared.State is their only communication medium. Node A writes the calculation results to State, and Node B reads them from State to continue processing. Schema is the "format specification" of this shared memory.
LangGraph is very flexible here - it doesn't force you to use a specific Python data type. As long as it is a structured type that Python can recognize, LangGraph can basically accept it:
Next, we will explain these three methods in depth one by one.
TypedDictfrom Pythontyping(ortyping_extensions) module. It allows you to define a "type-constrained dictionary" - essentially a dictionary, but with each key marked with the expected value type.
from typing_extensions import TypedDict
# 定义一个最简单的 TypedDict State
class TypedDictState(TypedDict):
foo: str # 字段 foo,期望类型是字符串
bar: str # 字段 bar,期望类型是字符串
The type annotation for TypedDict is simplyhint, noenforce. Static checking tools (such as mypy, Pyright) or IDEs will use these hints to detect potential errors in advance, but the Python interpreterWill not be verified at runtimethese types. You can definitely givefooAssign an integer, Python will not report an error.
For fields that can only take a few fixed values (such as status, emotion, mode), you can useLiteralType hints for constraints:
from typing import Literal
from typing_extensions import TypedDict
class TypedDictState(TypedDict):
name: str # 任意字符串
mood: Literal["happy", "sad"] # 只能是 "happy" 或 "sad"
hereLiteral["happy", "sad"]Tell the type checker:moodThe value of the field should only be one of these two strings. if you writemood: "angry", mypy will report an error - but Python doesn't care when running.
Pass the TypedDict class toStateGraph, and then define the node function. The access method is exactly the same as that of an ordinary dictionary - usestate["字段名"]:
import random
from typing import Literal
from langgraph.graph import StateGraph, START, END
# ─── 节点函数:用字典下标访问 state ───
def node_1(state):
print("---Node 1---")
# state["name"] 读取当前 name 字段的值
return {"name": state["name"] + " is ... "}
def node_2(state):
print("---Node 2---")
return {"mood": "happy"}
def node_3(state):
print("---Node 3---")
return {"mood": "sad"}
def decide_mood(state) -> Literal["node_2", "node_3"]:
if random.random() < 0.5:
return "node_2"
return "node_3"
# ─── 构建图 ───
builder = StateGraph(TypedDictState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()
# ─── 调用:传入初始 State 字典 ───
result = graph.invoke({"name": "Lance"})
# 输出可能是:{'name': 'Lance is ... ', 'mood': 'happy'}
TypedDict is part of the Python standard library,No additional installation required. Its access method is intuitive (dictionary subscript), consistent with the habits of most Python developers. For simple to moderately complex states, TypedDict is the lowest-friction option - that's why you'll see it in the official LangGraph documentation and in almost every tutorial.
PythondataclassesModules (built-in to Python 3.7+) provide another way to define structured data. it uses@dataclassDecorators are automatically generated__init__、__repr__and other magic methods that allow you to create "classes primarily used to store data" with a more concise syntax:
from dataclasses import dataclass
from typing import Literal
@dataclass
class DataclassState:
name: str
mood: Literal["happy", "sad"]
Compared with TypedDict, Dataclass has two significant differences:
state["name"], Dataclass is accessed using propertiesstate.name{"name": "Lance"}, Dataclass uses constructorDataclassState(name="Lance", mood="sad")Noticenode_1The way to access State has changed - from dictionary subscript to attribute access:
# TypedDict 版本(字典下标访问)
def node_1_typed_dict(state):
return {"name": state["name"] + " is ... "} # state["name"]
# Dataclass 版本(属性访问)
def node_1_dataclass(state):
return {"name": state.name + " is ... "} # state.name
Even though State is a Dataclass,The updated value returned by the node is still a dictionary, not a new Dataclass instance. This is because LangGraph internally maps each key in the dictionary returned by the node to each field of State for merge and update. As long as the key names of the dictionary are consistent with the attribute names of the Dataclass, LangGraph can handle it correctly.
# 构建图(和 TypedDict 版一样,只是 Schema 类不同)
builder = StateGraph(DataclassState)
builder.add_node("node_1", node_1_dataclass)
builder.add_node("node_2", node_2) # node_2/node_3 返回字典,通用
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()
# 调用:传入 Dataclass 实例而不是字典
result = graph.invoke(DataclassState(name="Lance", mood="sad"))
# 输出(LangGraph 返回字典格式):
# ---Node 1---
# ---Node 3---
# {'name': 'Lance is ... ', 'mood': 'sad'}
A practical advantage of Dataclass is that it can be specified directly on the field definitionDefault value, which is very convenient for scenarios where some fields "may not be passed":
from dataclasses import dataclass, field
@dataclass
class AgentDataclassState:
# 必填字段,无默认值
user_input: str
# 有默认值的字段——如果初始化时没传,就用默认值
messages: list = field(default_factory=list) # 默认空列表
iteration: int = 0 # 默认 0
is_done: bool = False # 默认 False
This is a classic pitfall in Python:Cannot be usedmessages: list = [], because this empty list is the same object shared by all instances, which can lead to unexpected state pollution.field(default_factory=list)Ensure that each instance is created with a new independent list.
TypedDict itself has no built-in default value mechanism (you need to provide all fields manually at initialization time, or cooperate withtotal=FalseMake the field optional). Therefore, if your State has many optional fields or fields with default values, Dataclass is more suitable than TypedDict.
Both TypedDict and Dataclass only provide typesTips, no checking is done at runtime. This means that even if you writemood: Literal["happy", "sad"], the following code will not report an error:
# Dataclass:即使 mood 不在 Literal 允许范围内,Python 也不会报错!
bad_instance = DataclassState(name="Lance", mood="mad")
print(bad_instance.mood) # 输出: mad — 没有任何错误
This is a hidden danger in a production environment: data validation failures will be silently ignored, and erroneous data will enter the graph execution process, eventually causing hard-to-track bugs in an unexpected place.
PydanticIt is the most popular data validation library in the Python ecosystem, and is widely used internally by LangChain and LangGraph. it is passed through inheritanceBaseModelto define the data model and inwhen instantiatedPerform type checking immediately:
from pydantic import BaseModel, field_validator, ValidationError
class PydanticState(BaseModel):
name: str
mood: str # "happy" 或 "sad"
# 自定义校验器:在实例化时自动执行
@field_validator("mood")
@classmethod
def validate_mood(cls, value):
if value not in ["happy", "sad"]:
raise ValueError("Each mood must be either 'happy' or 'sad'")
return value
When you try to create an instance with an invalid value, Pydantic will immediately throwValidationError:
try:
# mood="mad" 不在允许值范围内,立刻报错!
bad_state = PydanticState(name="John Doe", mood="mad")
except ValidationError as e:
print("Validation Error:", e)
# 输出:
# Validation Error: 1 validation error for PydanticState
# mood
# Input should be 'happy' or 'sad' [type=literal_error, input_value='mad', ...]
@field_validatorThe decorator registers a class method that is automatically called when Pydantic builds an instance:
@field_validator("mood"):Specify that this verification function acts onmoodField@classmethod:Pydantic requires field_validator to be a class method (clsis the class itself,valueis the value of the field)value(You can also do data cleaning here, such asreturn value.strip())ValueError, Pydantic will wrap it intoValidationErrorputPydanticStatepass toStateGraph, the rest of the code is exactly the same as the TypedDict/Dataclass version:
# 图的构建代码和之前完全一样,只是 Schema 换成了 PydanticState
builder = StateGraph(PydanticState)
builder.add_node("node_1", node_1_dataclass) # 属性访问方式,同 Dataclass
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_conditional_edges("node_1", decide_mood)
builder.add_edge("node_2", END)
builder.add_edge("node_3", END)
graph = builder.compile()
# 调用:传入 PydanticState 实例
result = graph.invoke(PydanticState(name="Lance", mood="sad"))
# 输出:{'name': 'Lance is ... ', 'mood': 'sad'}
Pydantic goes beyond basic type checking. You can also use it to:
from pydantic import BaseModel, Field, validator
from typing import Optional, List
class RobustAgentState(BaseModel):
# 带默认值的字段
user_input: str
messages: List[str] = [] # Pydantic 对列表默认值做了安全处理
# 可选字段(None 是合法值)
search_result: Optional[str] = None
# 用 Field 设置取值范围约束
max_iterations: int = Field(default=5, ge=1, le=20) # 1 <= value <= 20
# 字符串长度约束
session_id: str = Field(min_length=1, max_length=64)
LangChain itself uses Pydantic extensively:BaseChatModel、BaseMessage、BaseTooland other core classes all inherit from PydanticBaseModel. If you need to store LangChain’s message object in your State (HumanMessage、AIMessage), use Pydantic State for better type safety and IDE smart tips.
| characteristic | TypedDict | Dataclass | Pydantic BaseModel |
|---|---|---|---|
| Belonging library | Python standard library (typing) | Python standard library (dataclasses) | Third-party library (pydantic) |
| Field access method | state["name"] |
state.name |
state.name |
| Initialization method | {"name": "Lance"} |
MyState(name="Lance") |
MyState(name="Lance") |
| Type hint support | Yes (static checking only) | Yes (static checking only) | Yes (static + runtime) |
| Runtime type checking | None | None | Yes (verified at instantiation) |
| Default value support | Limited (requires total=False) | Native support | Native support |
| Custom verification logic | None | Manually implement __post_init__ | @field_validator native support |
| Serialization support | It’s a dictionary, naturally serializable | Requires dataclasses.asdict() | .model_dump() / .model_json() |
| Integrate with LangChain | good | good | Best (LangChain itself uses Pydantic) |
| learning cost | lowest | Low | Medium (need to learn Pydantic API) |
| Applicable scenarios | Prototype, teaching, simple Agent | Have default values, attribute style preferences | Production Agent, data verification is required |
chooseTypedDict. There is zero installation cost, the dictionary style is intuitive, and all LangGraph official examples use it.
chooseDataclass。field(default_factory=...)Than TypedDict'stotal=FalseMore elegant, and property access is more OOP-style.
choosePydantic BaseModel. Verification is performed immediately upon instantiation, and illegal data will not enter the execution process of the graph, reducing hard-to-find bugs caused by "data pollution".
All three methods are possible, butPydanticIntegration with LangChain is most natural because LangChain's message classes are themselves Pydantic models.
Don't use Pydantic in all scenarios just because "Pydantic has validation so it must be the best". In the teaching code and rapid iteration phase, TypedDict's lightweight is a real advantage - less code noise, faster prototype verification.Choose the right tool, not the most complicated one.
LangGraph has a very important internal mechanism:Each field of State is stored independently, rather than the entire State object being replaced as a whole.
This explains a seemingly strange phenomenon: even if State is a Dataclass or Pydantic model, the node still returns a dictionary -
# State 是 DataclassState,但节点依然返回字典
def node_2(state):
return {"mood": "happy"} # 只返回要改变的字段
# 不需要返回完整的 DataclassState 对象
LangGraph receives this dictionary{"mood": "happy"}Later, I willmoodFields are updated individually into the current State,Other fields (such asname) remains unchanged。
By default, when a node returns a new value for a field, the new value isdirect coverageOld value:
For fields like messages (message history), you want toAppendNew messages are added to the list instead of overwriting the entire list. This requires usingReducer——PassedAnnotatedSyntax for attaching functions to fields:
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class ChatState(TypedDict):
# Annotated[list, add_messages] 表示:
# - 这个字段是 list 类型
# - 更新策略是 add_messages(追加而非覆盖)
messages: Annotated[list, add_messages]
# 普通字段,默认覆盖
current_step: str
add_messagesIt is LangGraph's built-in reducer, specially used for the accumulation of message lists. It also intelligently handles message ID deduplication. The complete mechanism of reducer (including how to customize reducer) will be in Module 2Lesson 4:State ReducersExplained in detail. Here you can first establish a basic impression.
| stage | what happened | What developers need to do |
|---|---|---|
| Schema definition | Declare the fields, types, and update strategies in the graph | Define TypedDict / Dataclass / Pydantic classes |
| graph initialization | LangGraph reads Schema and understands the structure of State | Pass the Schema class toStateGraph(Schema) |
| invoke() call | Pass in the initial State and start execution | Pass in a dictionary or Schema instance |
| Node execution | The node function receives the current State and returns the updated dictionary | Write node functions, read state, and return the change dictionary |
| State merge | LangGraph merges the dictionary returned by the node into the current State (override by default, or use reducer) | Understand the difference between overwriting vs appending |
| Arrive at END | invoke() returns the final State (dictionary format) | Get the required fields from the returned dictionary |
state["field"]visit{...}state.fieldvisitMyState(...)field(default_factory=...)state.fieldvisit@field_validatorSupport custom verificationAnnotated[list, add_messages]Before defining Schema, first make a table: Which node will read which fields? Which node will write which fields? This table is the prototype of your Schema.
use user_questioninstead ofq; usesearch_resultsinstead ofresults. State is a shared contract for all nodes, and ambiguous naming will confuse developers of each node.
Use TypedDict to quickly iterate during the prototype stage, and then migrate to Pydantic to increase runtime guarantees after the Schema structure is stable. The migration cost between the two is not high - field definitions can basically be reused, mainly because the access method is changed from dictionary subscripts to attributes.
State is "data shared between nodes", not "all data". If a certain data is only used inside a certain node, it is just a local variable and does not need to be entered into the State. Keep State simple and store only the fields necessary for cross-node communication.
Any field that needs to be "accumulated" rather than "covered" must be usedAnnotated[..., add_messages]Or custom reducer. The most typical one is dialoguemessagesList - each round of dialogue needs to be appended and cannot be overwritten.
State Schema is not just "data structure definition", it is the entire graphdata contract. TypedDict gives you the lowest friction development experience, Dataclass gives you better default value management, and Pydantic gives you production-level data security. By understanding the differences between the three, you can make the correct selection decision based on the scenario - this is the first step in building a reliable LangGraph Agent and the basis of Module 2.