add_node: node protocols make the `state` parameter name load-bearing for type checkers
Node protocols declare state as positional-or-keyword, so its name is load-bearing for type checkers while the runtime passes it positionally.
Repro
mypy --strict, langgraph 1.2.11:
from collections.abc import Awaitable, Callable
from typing import Any, TypedDict
from langgraph.graph import StateGraph
class State(TypedDict):
x: int
async def named_state(state: State) -> dict[str, Any]:
return {}
async def named_data(data: State) -> dict[str, Any]:
return {}
NodeFn = Callable[[State], Awaitable[dict[str, Any]]]
def factory() -> NodeFn:
return named_state
def build() -> None:
g: StateGraph[State, Any, Any, Any] = StateGraph(State)
g.add_node("a", named_state) # ok
g.add_node("b", named_data) # error: incompatible type
g.add_node("c", factory()) # error: incompatible typeWithout the explicit StateGraph[...] parametrisation the same two lines fail as
no overload variant of "add_node" matches, which lists every overload and is
hard to act on.
Both nodes run fine — named_data returns {'x': 42}.
Cause
libs/langgraph/langgraph/graph/_node.py:
class _Node(Protocol[NodeInputT_contra]):
def __call__(self, state: NodeInputT_contra) -> Any: ...state is not positional-only, so:
- a node's first parameter must be named
state, although_internal/_runnable.pycalls it positionally (args = (input,), thenself.func(*args, **kwargs)); Callable[[State], Awaitable[...]]can never satisfy the protocol, sinceCallableparameters are positional-only — so typing a node factory with aCallablealias always fails, whatever the function is named.
Suggested fix
Mark the parameter positional-only in the nine node protocols:
def __call__(self, state: NodeInputT_contra, /) -> Any: ...This only widens what satisfies the protocol; nodes that already name it
state keep matching. config, writer, store and runtime are passed as
keyword arguments, so they are unaffected.
Verified locally: adding / to _Node alone makes all three lines above pass,
and the change is annotation-only (126 tests in a downstream project still
pass against the patched file).
Happy to open the PR if you would like it.
Source: langchain-ai/langgraph