LangChain LangGraph
Module 1 Module 2 Module 3 Module 4 Module 5 Module 6
Module 2 · State & Memory
1 2-1 State Schema
2 2-2 Reducers
3 2-3 Multi Schemas
4 2-4 Trim & Filter
5 2-5 Chatbot Summary
6 2-6 Ext Memory
SUM Module Summary
HomeLangGraphModule 2 · State & Memory2-1 State Schema
📓 Notebook 📖 Explained
LANGGRAPH MODULE 2 · LESSON 1

In-depth understanding of LangGraph
State Schema state schema definition

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.

1The goal of Module 2: In-depth understanding of State and Memory

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.

Two main lines of Module 2

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.

Why talk specifically about Schema definition?

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:

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.

2What is State Schema and why is it needed?

Schema is the "data contract" of the graph

When you create aStateGraph, the first step is to pass in aState Schema class

# 这里的 MyState 就是 Schema——它定义了这张图能存放什么数据
builder = StateGraph(MyState)

This Schema does three things:

Core Insight: State is the "shared memory" of the graph

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.

What Schema definition methods does LangGraph support?

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:

TypedDict
  • Python standard library
  • Dictionary style access
  • Only type hints, no verification
  • The lightest and most commonly used
Dataclass
  • Python standard library
  • Property style access
  • Only type hints, no verification
  • Support default value
Pydantic BaseModel
  • Third-party libraries
  • Property style access
  • Strong validation at runtime
  • The safest and strictest

Next, we will explain these three methods in depth one by one.

3TypedDict: the most commonly used way to define status

What is TypedDict?

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,期望类型是字符串
Important: type hints ≠ type coercion

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.

Using Literal to constrain enumeration values

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.

Using TypedDict in LangGraph

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'}
Why do LangGraph documentation and examples use TypedDict by default?

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.

Graph structure visualization with TypedDict

TypedDictState example graph structure

__start__
State: {name: "Lance"}
node_1
Write: name = "Lance is ... "
decide_mood() route
node_2
Write: mood = "happy"
node_3
Write: mood = "sad"
__end__
State: {name: "Lance is ... ", mood: "happy/sad"}

4Dataclass: More Pythonic structured data

Introduction to Python Dataclasses

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:

Dataclass versions of node functions

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
Interesting detail: Node still returns a dictionary

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.

Call Dataclass graph

# 构建图(和 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'}

Advantages of Dataclass: Support default values

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
Why do we need to use field(default_factory=list) for list default values?

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.

5Pydantic BaseModel: strong validation at runtime

Fundamental flaws of TypedDict/Dataclass

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.

Pydantic: A solution for runtime verification

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', ...]

How @field_validator works

@field_validatorThe decorator registers a class method that is automatically called when Pydantic builds an instance:

Seamlessly use Pydantic State in LangGraph

putPydanticStatepass 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'}

More powerful features of Pydantic

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)
Deep integration of Pydantic and LangChain

LangChain itself uses Pydantic extensively:BaseChatModelBaseMessageBaseTooland other core classes all inherit from PydanticBaseModel. If you need to store LangChain’s message object in your State (HumanMessageAIMessage), use Pydantic State for better type safety and IDE smart tips.

6Horizontal comparison and selection suggestions of three methods

Function comparison table

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

How to choose? decision tree

Most common misunderstandings

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.

7Detailed explanation of the circulation mechanism of State in the graph

Fields are stored independently

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

Default update behavior: Overwrite

By default, when a node returns a new value for a field, the new value isdirect coverageOld value:

name: "Lance"
mood: "sad"
node_1 before execution
name: "Lance is ... "
mood: "sad"
After node_1 is executed (name is overwritten, mood remains unchanged)
name: "Lance is ... "
mood: "sad"
node_3 before execution
name: "Lance is ... "
mood: "sad"
After node_3 is executed (mood is overwritten but the value is the same, name remains unchanged)

When you need to "append" rather than "overwrite": reducer

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
messages: [HumanMsg("Hello")]
Before LLM node execution
messages: [HumanMsg("Hello"),
AIMsg("Hello!")]
After LLM node execution (append, not overwrite)
reducer will be explained in depth in subsequent courses of Module 2

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.

The complete life cycle of State

stagewhat happenedWhat 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

8Summary: Best Practices in State Schema Design

Key conclusions at a glance

Key points of TypedDict
  • • Use dictionary subscriptsstate["field"]visit
  • • Type annotations are hints and are not verified at runtime.
  • • Pass dictionary when invoke(){...}
  • • Lightest, most commonly used for teaching/prototyping
Dataclass points
  • • Use attributesstate.fieldvisit
  • • Type annotations are hints and are not verified at runtime.
  • • Pass instance when invoke()MyState(...)
  • • Native support for default valuesfield(default_factory=...)
Pydantic Essentials
  • • Use attributesstate.fieldvisit
  • • Strong verification during instantiation, if illegal, an error will be reported immediately
  • @field_validatorSupport custom verification
  • • The first choice for production Agent, deeply compatible with LangChain
common rules
  • • Nodealways returns a dictionary, no matter what type the Schema is
  • • The default update strategy for fields iscover
  • • For additionalAnnotated[list, add_messages]
  • • Each field of State is stored and updated independently

Practical advice for designing a State Schema

This lesson is located in Module 2

Lesson 1 (this lesson) Three ways to define State Schema (TypedDict / Dataclass / Pydantic)
Lesson 2 State Schema Advanced: Multiple Schemas and Private States (Multiple Schemas)
Lesson 3 Trim/Filter Messages
Lesson 4 State reducers: Control the update strategy of fields (override vs append vs custom)
Lesson 5 Chatbot: Conversation Summarization
Lesson 6 External Memory: Chatbot with External Memory
final conclusion

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.