07 Oct LangGraph State: TypedDict, Pydantic, and Reducers
State is the heart of every graph. This chapter covers how state is updated and how to choose the right way to define it.
Default behavior: overwrite
When a node returns a value for a field, LangGraph replaces the old value.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
steps: list[str]
def node_a(state: State):
return {"steps": ["a"]}
def node_b(state: State):
return {"steps": ["b"]}
builder = StateGraph(State)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()
print(graph.invoke({"steps": ["start"]}))
Output:
{'steps': ['b']}
Node B wiped out everything before it. That is fine for a single value like answer, but bad for a history or log.
Reducers: control how updates merge
A reducer tells LangGraph how to combine the old value with the new one. You attach it using Annotated:
import operator
from typing import Annotated, TypedDict
class State(TypedDict):
steps: Annotated[list[str], operator.add]
operator.add on lists means “append”. Run the same graph with this state:
Output:
{'steps': ['start', 'a', 'b']}
Now every node adds to the list instead of replacing it. You will rely on this for chat history in Chapter 9.
Writing your own reducer
A reducer is just a function that takes the current value and the new value, and returns the merged result:
from typing import Annotated, TypedDict
def keep_highest(current: int, new: int) -> int:
return max(current, new)
class State(TypedDict):
score: Annotated[int, keep_highest]
If one node returns {“score”: 5} and the next returns {“score”: 3}, the final score stays 5.
Pydantic: state with validation
TypedDict only describes the shape of your data and doesn’t check it. Pydantic checks values and raises an error for bad input.
from pydantic import BaseModel, Field, ValidationError
from langgraph.graph import StateGraph, START, END
class State(BaseModel):
name: str
age: int = Field(ge=0) # must be 0 or more
def birthday(state: State):
return {"age": state.age + 1}
builder = StateGraph(State)
builder.add_node("birthday", birthday)
builder.add_edge(START, "birthday")
builder.add_edge("birthday", END)
graph = builder.compile()
print(graph.invoke({"name": "Asha", "age": 20}))
try:
graph.invoke({"name": "Asha", "age": -5})
except ValidationError:
print("Validation failed: age cannot be negative")
Output:
{'name': 'Asha', 'age': 21}
Validation failed: age cannot be negative
Things to notice:
- Inside nodes, you read fields as attributes: state.age, not state[“age”]
- Nodes still return a plain dictionary of updates
- The final result from invoke() is a dictionary, not a Pydantic object
- Validation runs on the data coming into nodes, not on what nodes return
Which one should you choose?
- TypedDict: simple, fast, and the most common. Best for most tutorials and apps.
- Pydantic BaseModel: validates data and gives clear errors. Best for apps that accept user input.
- dataclass: attribute access with defaults. Use it if you prefer that style.
Recommendation: start with TypedDict, and switch to Pydantic when bad input becomes a real risk.
Common mistakes
- Expecting a list to grow without a reducer: without Annotated[…, operator.add], the list is overwritten
- Returning the full list when using operator.add: return only the new items, or you will get duplicates
- Using state[“age”] with Pydantic: use state.age instead
- Putting the reducer on the wrong field: it applies only to the field it is attached to
Summary
- By default, a returned value replaces the old one
- A reducer (via Annotated) controls how updates are merged
- operator.add appends to lists, and you can write custom reducers
- Pydantic adds validation, while TypedDict keeps things simple
Practice
- Build a graph with three nodes that each add a string to a log list using operator.add, then print the final log
- Write a reducer called keep_lowest and test it with two nodes returning different numbers
- Change the Pydantic example to also require name to be at least 2 characters (hint: Field(min_length=2))
If you liked the tutorial, spread the word and share the link and our website, Studyopedia, with others.
For Videos, Join Our YouTube Channel:Â Join Now
Read More:
- Generative AI Tutorial
- AI Ethics
- Machine Learning Tutorial
- Deep Learning Tutorial
- Ollama Tutorial
- Retrieval Augmented Generation (RAG) Tutorial
- ChatGPT Tutorial
- Microsoft Copilot Tutorial
No Comments