# Python implementation reference Use this reference only for LangGraph projects written in Python. The shared concepts, decisions, and invariants remain in `../SKILL.md`. ## Contents - [State Management](#state-management) - [Nodes](#nodes) - [Edges](#edges) - [Command](#command) - [Send API](#send-api) - [Running Graphs: Invoke and Stream](#running-graphs-invoke-and-stream) - [Error Handling](#error-handling) - [Common Fixes](#common-fixes) ## State Management ### State with reducer Define state schema with reducers for accumulating lists and summing integers. ```python from typing_extensions import TypedDict, Annotated import operator class State(TypedDict): name: str # Default: overwrites on update messages: Annotated[list, operator.add] # Appends to list total: Annotated[int, operator.add] # Sums integers ``` ### Forgot reducer for list Without a reducer, returning a list overwrites previous values. ```python # WRONG: List will be OVERWRITTEN class State(TypedDict): messages: list # No reducer! # Node 1 returns: {"messages": ["A"]} # Node 2 returns: {"messages": ["B"]} # Final: {"messages": ["B"]} # "A" is LOST! # CORRECT: Use Annotated with operator.add from typing import Annotated import operator class State(TypedDict): messages: Annotated[list, operator.add] # Final: {"messages": ["A", "B"]} ``` ### Return partial state updates Nodes must return partial updates, not mutate and return full state. ```python # WRONG: Returning entire state object def my_node(state: State) -> State: state["field"] = "updated" return state # Don't mutate and return! # CORRECT: Return dict with only the updates def my_node(state: State) -> dict: return {"field": "updated"} ``` ## Nodes ### Node function signatures | Signature | When to Use | |-----------|-------------| | `def node(state: State)` | Simple nodes that only need state | | `def node(state: State, config: RunnableConfig)` | Need thread_id, tags, or configurable values | | `def node(state: State, runtime: Runtime[Context])` | Need runtime context, store, or stream_writer | ```python from langchain_core.runnables import RunnableConfig from langgraph.runtime import Runtime def plain_node(state: State): return {"results": "done"} def node_with_config(state: State, config: RunnableConfig): thread_id = config["configurable"]["thread_id"] return {"results": f"Thread: {thread_id}"} def node_with_runtime(state: State, runtime: Runtime[Context]): user_id = runtime.context.user_id return {"results": f"User: {user_id}"} ``` ## Edges ### Basic graph Simple two-node graph with linear edges. ```python from langgraph.graph import StateGraph, START, END from typing_extensions import TypedDict class State(TypedDict): input: str output: str def process_input(state: State) -> dict: return {"output": f"Processed: {state['input']}"} def finalize(state: State) -> dict: return {"output": state["output"].upper()} graph = ( StateGraph(State) .add_node("process", process_input) .add_node("finalize", finalize) .add_edge(START, "process") .add_edge("process", "finalize") .add_edge("finalize", END) .compile() ) result = graph.invoke({"input": "hello"}) print(result["output"]) # "PROCESSED: HELLO" ``` ### Conditional edges Route to different nodes based on state with conditional edges. ```python from typing import Literal from langgraph.graph import StateGraph, START, END class State(TypedDict): query: str route: str result: str def classify(state: State) -> dict: if "weather" in state["query"].lower(): return {"route": "weather"} return {"route": "general"} def route_query(state: State) -> Literal["weather", "general"]: return state["route"] graph = ( StateGraph(State) .add_node("classify", classify) .add_node("weather", lambda s: {"result": "Sunny, 72F"}) .add_node("general", lambda s: {"result": "General response"}) .add_edge(START, "classify") .add_conditional_edges("classify", route_query, ["weather", "general"]) .add_edge("weather", END) .add_edge("general", END) .compile() ) ``` ## Command ### Command state and routing Command lets you update state AND choose next node in one return. ```python from langgraph.types import Command from typing import Literal class State(TypedDict): count: int result: str def node_a(state: State) -> Command[Literal["node_b", "node_c"]]: """Update state AND decide next node in one return.""" new_count = state["count"] + 1 if new_count > 5: return Command(update={"count": new_count}, goto="node_c") return Command(update={"count": new_count}, goto="node_b") graph = ( StateGraph(State) .add_node("node_a", node_a) .add_node("node_b", lambda s: {"result": "B"}) .add_node("node_c", lambda s: {"result": "C"}) .add_edge(START, "node_a") .add_edge("node_b", END) .add_edge("node_c", END) .compile() ) ``` ## Send API ### Orchestrator worker Fan out tasks to parallel workers using the Send API and aggregate results. ```python from langgraph.types import Send from typing import Annotated import operator class OrchestratorState(TypedDict): tasks: list[str] results: Annotated[list, operator.add] summary: str def orchestrator(state: OrchestratorState): """Fan out tasks to workers.""" return [Send("worker", {"task": task}) for task in state["tasks"]] def worker(state: dict) -> dict: return {"results": [f"Completed: {state['task']}"]} def synthesize(state: OrchestratorState) -> dict: return {"summary": f"Processed {len(state['results'])} tasks"} graph = ( StateGraph(OrchestratorState) .add_node("worker", worker) .add_node("synthesize", synthesize) .add_conditional_edges(START, orchestrator, ["worker"]) .add_edge("worker", "synthesize") .add_edge("synthesize", END) .compile() ) result = graph.invoke({"tasks": ["Task A", "Task B", "Task C"]}) ``` ### Send accumulator Use a reducer to accumulate parallel worker results (otherwise last worker overwrites). ```python # WRONG: No reducer - last worker overwrites class State(TypedDict): results: list # CORRECT class State(TypedDict): results: Annotated[list, operator.add] # Accumulates ``` ## Running Graphs: Invoke and Stream ### Invoke basics ```python result = graph.invoke({"input": "hello"}) # With config (for persistence, tags, etc.) result = graph.invoke({"input": "hello"}, {"configurable": {"thread_id": "1"}}) ``` ### Stream llm tokens Stream LLM tokens in real-time for chat UI display. ```python for chunk in graph.stream( {"messages": [HumanMessage("Hello")]}, stream_mode="messages" ): token, metadata = chunk if hasattr(token, "content"): print(token.content, end="", flush=True) ``` ### Stream custom data Emit custom progress updates from within nodes using the stream writer. ```python from langgraph.config import get_stream_writer def my_node(state): writer = get_stream_writer() writer("Processing step 1...") # Do work writer("Complete!") return {"result": "done"} for chunk in graph.stream({"data": "test"}, stream_mode="custom"): print(chunk) ``` ## Error Handling ### Retry policy Use RetryPolicy for transient errors (network issues, rate limits). ```python from langgraph.types import RetryPolicy workflow.add_node( "search_documentation", search_documentation, retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0) ) ``` ### Tool node error handling Use ToolNode from langgraph.prebuilt to handle tool execution and errors. When handle_tool_errors=True, errors are returned as ToolMessages so the LLM can recover. ```python from langgraph.prebuilt import ToolNode tool_node = ToolNode(tools, handle_tool_errors=True) workflow.add_node("tools", tool_node) ``` ## Common Fixes ### Compile before execution Must compile() to get executable graph. ```python # WRONG builder.invoke({"input": "test"}) # AttributeError! # CORRECT graph = builder.compile() graph.invoke({"input": "test"}) ``` ### Infinite loop needs exit Provide conditional path to END to avoid infinite loops. ```python # WRONG: Loops forever builder.add_edge("node_a", "node_b") builder.add_edge("node_b", "node_a") # CORRECT def should_continue(state): return END if state["count"] > 10 else "node_b" builder.add_conditional_edges("node_a", should_continue) ``` ### Additional common mistakes ```python # Router must return names of nodes that exist in the graph builder.add_node("my_node", func) # Add node BEFORE referencing in edges builder.add_conditional_edges("node_a", router, ["my_node"]) # Command return type needs Literal for routing destinations (Python) def node_a(state) -> Command[Literal["node_b", "node_c"]]: return Command(goto="node_b") # START is entry-only - cannot route back to it builder.add_edge("node_a", START) # WRONG! builder.add_edge("node_a", "entry") # Use a named entry node instead # Reducer expects matching types return {"items": ["item"]} # List for list reducer, not a string ```