Chapter 37 min read

Chapter 3: State Fundamentals

In Chapter 2, we established the fundamental mental model of LangGraph: State → Node → Edge.

We briefly introduced TypedDict and showed how nodes return dictionary updates to mutate the global state. But in a massive enterprise application involving 50 nodes and looping cycles, a simple Python dictionary is not enough.

What happens if a node forgets to populate a key? What happens if two nodes try to update the exact same key at the exact same time?

In this chapter, we are going to dive into the absolute beating heart of LangGraph: The State Layer. We will master TypedDict, Pydantic validation, Optional Keys, Merge Semantics, and Production State Design.


3.1 Why State Is Everything

If you get your State schema wrong, your graph will eventually crash, hallucinate, or become impossible to debug.

State is the single source of truth for your entire application.

Think of the State as a persistent database row that travels through the graph. Nodes read from the row, do work, and write updates back to the row. Because the Graph Runtime controls exactly how those writes are merged, you must explicitly design your state schema to handle the specific needs of your workflow.


3.2 TypedDict

The most common way to define state in LangGraph is using Python's native TypedDict. TypedDict is lightweight, built into the standard library (from typing import TypedDict), and integrates perfectly with LangGraph's internal engine.

from typing import TypedDict

class SupportState(TypedDict):
    ticket_id: int
    user_query: str
    account_tier: str 
    is_resolved: bool

The Initial State

When you invoke a graph, you must provide the initial values. Any fields you do not provide will be missing from the dictionary.

# The graph starts with only the user query.
app.invoke({"user_query": "My router is broken", "is_resolved": False})

Crucial Note: TypedDict provides type hints, not runtime validation. If a node returns {"ticket_id": "string_instead_of_int"}, Python will not crash at runtime. It will blindly accept the string.


3.3 Pydantic (Runtime Validation)

If you are building an enterprise application and require absolute strictness, LangGraph allows you to use Pydantic classes instead of TypedDict.

Pydantic enforces strict runtime type validation. If a node attempts to update an integer field with a string, Pydantic will instantly throw a ValidationError and crash the graph.

from pydantic import BaseModel, Field

class SecureSupportState(BaseModel):
    ticket_id: int = Field(default=0, ge=0) # Must be an integer >= 0
    user_query: str
    is_resolved: bool = False

Industry Context: While Pydantic is fully supported, TypedDict remains the most common choice in production. It is lightweight, integrates naturally with Python typing, and avoids the performance overhead of running validation checks on every single node transition. Do not assume TypedDict is for beginners and Pydantic is for professionals.


3.4 Optional Keys and The Dreaded KeyError

One of the most common mistakes beginners make is assuming that because a field is in the TypedDict, it exists in memory.

from typing import TypedDict, Optional

class UserState(TypedDict):
    user_id: int
    response: Optional[str] # This field starts empty!

If your graph starts without a response, and a Node tries to read it like this:

def check_response(state: UserState):
    # CRASH! KeyError: 'response'
    if state["response"] == "yes":
        pass

The graph will crash instantly. Because response was never initialized, the key does not exist in the dictionary yet.

The Fix: Always use .get() for optional fields.

def check_response(state: UserState):
    # Safe! Returns None if the key doesn't exist yet.
    if state.get("response") == "yes":
        pass

3.5 State Updates and Merge Semantics

How does the Graph Runtime actually apply the dictionaries returned by your nodes to the global State? It uses a concept called Merge Semantics.

By default, LangGraph uses Overwrite Semantics.

Let's visualize this.

Current State:

{
  "status": "running"
}

Node B Performs Work and Returns:

{
  "status": "failed"
}

The Graph Runtime Merges the Result:

   ↓ Overwrites existing key
{
  "status": "failed"
}

If a node returns a key that already exists, the old value is permanently destroyed. If a node returns a key that does not exist, it is safely added to the state. Untouched keys are left alone.


3.6 Designing Production State (Flat vs Nested)

When designing your state for a production application, you will face a critical architectural decision: Should your state be flat or nested?

The Bad Way: Deeply Nested

# ANTI-PATTERN: Hard to read, hard to update
class BadState(TypedDict):
    user: dict # Contains {id, name, email, preferences: {theme, language}}
    session: dict # Contains {token, expiry, ip_address}

If a node wants to update the user's language, it has to return a massive nested dictionary update. Python's default dictionary merging struggles with deep nesting. state["user"]["preferences"]["theme"] becomes a nightmare to update without accidentally overwriting the rest of the user object.

The Good Way: Flat and Explicit

# PRO-PATTERN: Flat, predictable, explicitly typed
class GoodState(TypedDict):
    user_id: int
    user_email: str
    user_theme: str
    session_token: str

Keep your state as flat as possible. If a node needs to update the theme, it simply returns {"user_theme": "dark"}. It is clean, predictable, and immune to nesting merge bugs.


3.7 Common State Anti-Patterns

Avoid these fatal mistakes when designing your State:

  1. Putting Large Objects in State: The state will eventually be saved to a database (Checkpointing). Do not put a massive 50MB PDF string directly in the state. Save the PDF to an S3 bucket and put the s3_url: str in the state instead.
  2. Putting Unserializable Objects in State: Never put active Database Connection Pools, Boto3 AWS Clients, or Socket connections in the State. The State must be JSON-serializable so it can be saved and resumed.
  3. Returning the Entire State: Beginners often have a node read the state, modify it, and return the entire state dictionary. Do not do this. Nodes should ONLY return the exact keys they are updating. Returning the entire state wastes memory and can accidentally overwrite data modified by parallel nodes.

3.8 Building a Basic State Validation Graph

Let's build a functional, non-AI graph that focuses purely on State updates and Overwrite Semantics.

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Optional

# 1. Flat, safe state design
class TicketState(TypedDict):
    ticket_id: int
    ticket_text: str
    priority: Optional[str]

# 2. Node A: Reads text, updates priority
def classify_priority(state: TicketState):
    # Safely get text
    text = state.get("ticket_text", "")
    
    # Simple deterministic logic
    priority = "high" if "urgent" in text.lower() else "low"
    
    # Return ONLY the field we want to overwrite
    return {"priority": priority}

# 3. Node B: Reads priority, performs action
def log_ticket(state: TicketState):
    print(f"Logging Ticket {state['ticket_id']} with Priority: {state.get('priority')}")
    # Returning an empty dict means we don't want to update the state at all
    return {}

# 4. Build the Graph
builder = StateGraph(TicketState)

builder.add_node("classifier", classify_priority)
builder.add_node("logger", log_ticket)

builder.add_edge(START, "classifier")
builder.add_edge("classifier", "logger")
builder.add_edge("logger", END)

app = builder.compile()

# Execute!
result = app.invoke({
    "ticket_id": 101,
    "ticket_text": "My server is on fire, this is URGENT!"
})

print("Final State:", result)
# Final State: {'ticket_id': 101, 'ticket_text': 'My server is on fire...', 'priority': 'high'}

By understanding TypedDict, Flat Designs, Optional Keys, and Overwrite Semantics, you have mastered the foundational layer of LangGraph.

However, Overwrite Semantics have one massive flaw: They destroy lists.

In the next chapter, we will solve the list problem by diving into Reducers and State Aggregation.

    Chapter 3: State Fundamentals — Mastering LangGraph: From State Machines to Production AI Agents | Krishna Tiwari