# LangGraph 'Could not reconstruct builtins.set from checkpoint': why a set of tuples comes back as a list

*Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this article with AI assistance and checked it against the GitHub issue, the `langgraph-checkpoint` source, the published LangGraph packages and the MemorySync docs listed under Sources on Nov 1, 2026, and rechecked the issue, the PyPI releases and `jsonplus.py` on `main` on Nov 7, 2026. Code status: I ran every code block on this page offline with outbound connections blocked and no API keys, on Linux with `langgraph` 1.2.20, `langgraph-checkpoint` 4.3.1 and `langgraph-checkpoint-sqlite` 3.1.3 (the latest releases on Nov 7, Python 3.13.18), and earlier with `langgraph` 1.2.19 (Python 3.13.5). I also ran the reproduction and the fix on `langgraph-checkpoint` 4.3.0 and 4.2.0. No model is called in these graphs. I didn't run anything on Windows or macOS, or with the Postgres or Redis checkpointers, and I didn't run the MemorySync snippet against the API (offline, its first call failed on the blocked network).*

Your LangGraph state has a field like `visited: set[tuple[int, int]]`. Everything looks fine until LangGraph reads that state back from a checkpoint, for example when the graph pauses on `interrupt()` and you resume it. Then the log says:

```text
Could not reconstruct builtins.set from checkpoint (TypeError); returning its serialized data instead.
```

And the set is now a list of lists, `[[0, 1], [0, 0]]`. That's LangGraph issue [#9516](https://github.com/langchain-ai/langgraph/issues/9516), "Set of tuples in state is restored as a list from a checkpoint".

Short version: **the checkpoint serializer can't rebuild a set whose items are tuples.** The tuples are saved as lists, a list can't go in a set, so the rebuild fails and you get the saved list back. On `langgraph-checkpoint` 4.2.0 it was quieter: no warning, and the field came back as `None`. Until the serializer changes, **keep the field as a list of `[row, col]` lists in state and build the set inside your node** (Step 2). That worked on every version I tried.

MemorySync doesn't fix this error. It's about how LangGraph's checkpoint serializer stores a Python type, and MemorySync isn't a checkpointer. It comes up once, near the end, for a different job: long-term memory across threads.

> **Skip the plumbing for cross-thread memory.** The [MemorySync LangGraph guide](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint) shows `MemorySyncStore`, a LangGraph `BaseStore` for memory that outlives a thread. Your checkpointer still owns thread state, including this field. When you're ready, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

## Status (Nov 7, 2026)

- [#9516](https://github.com/langchain-ai/langgraph/issues/9516) was opened on Oct 31, 2026 (21:35 IST) by an outside contributor, with a reproduction on `langgraph` 1.2.19 and `langgraph-checkpoint` 4.3.1. It's still open with 4 comments and no assignee: two other users confirmed it independently (one on macOS, one on `main`), and the reporter opened a fix as PR [#9572](https://github.com/langchain-ai/langgraph/pull/9572) on Nov 4 (turn lists inside a decoded `set` or `frozenset` back into tuples). A bot closed that PR automatically within a minute because external contributors must be assigned to the issue first; it wasn't merged. The reporter has asked to be assigned. No maintainer has commented on the issue.
- The latest releases on PyPI on Nov 7, [`langgraph-checkpoint` 4.3.1](https://pypi.org/project/langgraph-checkpoint/) (published Oct 27, 2026, 01:27 IST) and `langgraph` 1.2.20 (Nov 5, 07:50 IST), behave as the issue says. `jsonplus.py` on the `main` branch was unchanged on Nov 7 (its last change is from Oct 9), so no fix has landed.
- The warning is new in 4.3.0 (Oct 13, 2026). Comparing the 4.2.0 and 4.3.0 source, 4.2.0 returned `None` for any value it couldn't rebuild; 4.3.0 added a fallback that logs this warning and returns the saved data instead, with a code comment saying `None` "would silently erase the value from restored state."
- In my runs, `InMemorySaver` and `SqliteSaver` (resuming in a second process) behaved the same on each version.

## Why it happens

The checkpointer doesn't keep your Python objects. It serializes state with LangGraph's `JsonPlusSerializer`, which uses msgpack. A `set` or `frozenset` is saved as its type name plus its items, and msgpack has no tuple type, so each `(0, 1)` is written as an array and read back as the list `[0, 1]`. When the checkpoint is loaded, the serializer calls `set([[0, 1], [0, 0]])`, which raises `TypeError: unhashable type: 'list'`, and the fallback hands you the list instead.

What I saw with the same serializer on 4.3.1:

| Value in state | Read back from the checkpoint |
|---|---|
| `{1, 2}` or `{"a", "b"}` | the same set |
| `{(0, 0), (0, 1)}` | `[[0, 1], [0, 0]]`, with the warning |
| `frozenset({(1, 2)})` | `[[1, 2]]`, with the same warning for `builtins.frozenset` |
| `[(1, 2)]` or `(1, 2)` | lists: `[[1, 2]]`, `[1, 2]` (no warning) |
| `{(1, 2): "x"}` (dict with tuple keys) | the same dict |

So sets of strings and ints are fine. Tuples anywhere come back as lists, but only inside a set does that make the rebuild fail. Passing `JsonPlusSerializer(pickle_fallback=True)` didn't change the result: the set still came back as a list. The warning is logged once per type per process, so in a busy app you may see it once and miss it.

## Step 1: Reproduce it

This is the reproduction from the issue, which I ran as-is:

```python
from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt


class State(TypedDict):
    visited: set[tuple[int, int]]


def explore(state: State) -> State:
    return {"visited": {(0, 0), (0, 1)}}


def ask(state: State) -> State:
    interrupt("continue?")
    return {}


builder = StateGraph(State)
builder.add_node("explore", explore)
builder.add_node("ask", ask)
builder.add_edge(START, "explore")
builder.add_edge("explore", "ask")
builder.add_edge("ask", END)
graph = builder.compile(checkpointer=InMemorySaver())

config = {"configurable": {"thread_id": "1"}}
graph.invoke({"visited": set()}, config)

visited = graph.get_state(config).values["visited"]
print(type(visited).__name__, visited)

result = graph.invoke(Command(resume=True), config)
print(type(result["visited"]).__name__, result["visited"])
```

Output on 4.3.1 (4.3.0 is the same):

```text
Could not reconstruct builtins.set from checkpoint (TypeError); returning its serialized data instead.
list [[0, 1], [0, 0]]
list [[0, 1], [0, 0]]
```

On 4.2.0, no warning:

```text
NoneType None
NoneType None
```

The node code after the resume is where it breaks. When the `ask` node checked `(0, 0) in state["visited"]` after the resume, it got `False` on 4.3.1, because a list of lists doesn't contain a tuple, and then `state["visited"].add(...)` raised `AttributeError: 'list' object has no attribute 'add'`. On 4.2.0 the same check raised `TypeError: argument of type 'NoneType' is not iterable`.

## Step 2: Keep a list in state, build the set in the node

Store what the serializer can round-trip exactly, a list of `[row, col]` lists, and turn it into a set of tuples only inside the node that uses it:

```python
from typing import TypedDict

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.types import Command, interrupt

Cell = tuple[int, int]


class State(TypedDict):
    # A sorted list of [row, col] pairs. Lists of ints survive a checkpoint as-is.
    visited: list[list[int]]


def load_cells(state: State) -> set[Cell]:
    return {(row, col) for row, col in state.get("visited") or []}


def dump_cells(cells: set[Cell]) -> list[list[int]]:
    return [list(cell) for cell in sorted(cells)]


def explore(state: State) -> State:
    cells = load_cells(state) | {(0, 0), (0, 1)}
    return {"visited": dump_cells(cells)}


def ask(state: State) -> State:
    interrupt("continue?")
    cells = load_cells(state)  # a real set again, after the resume
    print("(0, 0) seen:", (0, 0) in cells)
    cells.add((1, 1))
    return {"visited": dump_cells(cells)}


builder = StateGraph(State)
builder.add_node("explore", explore)
builder.add_node("ask", ask)
builder.add_edge(START, "explore")
builder.add_edge("explore", "ask")
builder.add_edge("ask", END)
graph = builder.compile(checkpointer=InMemorySaver())

if __name__ == "__main__":
    config = {"configurable": {"thread_id": "1"}}
    graph.invoke({"visited": []}, config)
    print(graph.get_state(config).values["visited"])

    result = graph.invoke(Command(resume=True), config)
    print(result["visited"])
```

Output, the same on 4.3.1, 4.3.0 and 4.2.0, with no warning:

```text
[[0, 0], [0, 1]]
(0, 0) seen: True
[[0, 0], [0, 1], [1, 1]]
```

What each part does:

- `visited: list[list[int]]` is a type that comes back from a checkpoint exactly as it went in.
- `load_cells()` rebuilds the set of tuples at the start of a node, so lookups like `(0, 0) in cells` work again.
- `dump_cells()` sorts before saving, so the same set is always saved as the same list, which makes checkpoints easier to compare.
- Every node that writes the field returns the whole list. If several nodes or parallel branches write it in one step, you'll need a reducer on the field. I didn't test one here.

Threads saved before the change still hold the set. I paused a thread with the Step 1 graph, then resumed it in a new process with the Step 2 graph (same field name, `SqliteSaver`). On 4.3.1 and 4.3.0, `load_cells()` read the returned `[[0, 1], [0, 0]]` list, so the old cells carried over (the warning still appeared once for that old checkpoint). On 4.2.0 the old value came back as `None`, `load_cells()` treated it as an empty set, and the resumed run ended with only `[[1, 1]]`: the old cells were lost.

## Step 3: Check it across a restart

`InMemorySaver` keeps everything in one process. To check the fix survives a real restart, I compiled the same graph with `SqliteSaver` and ran the start and the resume as two separate processes:

```python
# Run twice as separate processes: `python step3_restart.py start`, then `python step3_restart.py resume`.
import sqlite3
import sys

from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.types import Command

import step2_fix

conn = sqlite3.connect("checkpoints.db", check_same_thread=False)
graph = step2_fix.builder.compile(checkpointer=SqliteSaver(conn))
config = {"configurable": {"thread_id": "maze-42"}}

if sys.argv[1] == "start":
    graph.invoke({"visited": []}, config)
    print("paused with", graph.get_state(config).values["visited"])
else:
    print("resumed with", graph.invoke(Command(resume=True), config)["visited"])
```

Run `python step3_restart.py start`, then `python step3_restart.py resume`. Output (needs `pip install langgraph-checkpoint-sqlite`):

```text
paused with [[0, 0], [0, 1]]
(0, 0) seen: True
resumed with [[0, 0], [0, 1], [1, 1]]
```

I got that on all three versions. The Step 1 graph with `SqliteSaver` printed the warning and the list (or `None` on 4.2.0) in both processes, the same as with `InMemorySaver`.

## What about a Pydantic state?

If your state is a Pydantic `BaseModel` with `visited: set[tuple[int, int]]`, my run on 4.3.1 was mixed: the node after the resume received a real set of tuples, because Pydantic converted the list when it built the state, but the warning still appeared and `graph.get_state(config).values["visited"]` was still a list. On 4.2.0 the same graph failed on resume with a Pydantic `ValidationError` ("Input should be a valid set", input `None`). Step 2 works the same way on every version, so I'd use it either way.

## Skip the plumbing with MemorySync

Disclosure: I build MemorySync.

It doesn't fix this, and it doesn't replace your checkpointer. The [MemorySync LangGraph guide](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint) says thread state (checkpoints, resume, time travel) belongs to a checkpointer such as `langgraph-checkpoint-postgres`, `-sqlite` or `-redis`, so a set in your graph state goes through the same serializer whether you use MemorySync or not. What MemorySync adds is long-term memory across threads: `MemorySyncStore`, a LangGraph `BaseStore` you compile in with `store=`, plus pre-model context injection and a search tool. That helps if the data in your state is really something about a user that should carry over to their next conversation.

From the guide:

**Unverified (not run):**

```bash
pip install "langchain-memorysync[langgraph]"
```

```python
from langchain_memorysync.langgraph import MemorySyncStore

store = MemorySyncStore()  # MEMORYSYNC_API_KEY from the env

store.put(("memories", "user-1"), "diet", {"text": "vegetarian"})

item = store.get(("memories", "user-1"), "diet")
print(item.value)  # {'text': 'vegetarian'}

hits = store.search(("memories", "user-1"), query="what do they eat?")
print(hits[0].value, hits[0].score)

# Filters use the same JSONB-style operators as LangGraph's own stores.
store.search(("memories", "user-1"), filter={"confidence": {"$gte": 0.8}})
```

*Snippet adapted from the [MemorySync LangGraph guide](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint). With `langchain-memorysync` 1.2.0, I ran it offline with a placeholder key and the first call failed on the blocked network, as expected. I haven't run it against the API.*

**Try it:** MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint), create an API key in the dashboard and drop the snippet into your agent.

## Troubleshooting

- **The warning names `builtins.frozenset`.** Same cause, same fix: a frozenset of tuples comes back as a list.
- **No warning, but the field is `None` after a resume.** Check `pip show langgraph-checkpoint`. Before 4.3.0, a value the serializer couldn't rebuild came back as `None`.
- **`AttributeError: 'list' object has no attribute 'add'` after an interrupt.** The node got the list back. Use Step 2, or convert with `{tuple(x) for x in value}` at the start of the node.
- **The warning appeared once and never again.** It's logged once per type per process. Check other threads too.
- **A different type in the warning.** This page only covers sets and frozensets of tuples. Other "Could not reconstruct" warnings have other causes I didn't test.

## Common questions

**Will LangGraph fix this?**
Maybe. The issue is open and the reporter has proposed a fix, but the reporter's PR was auto-closed unmerged and no maintainer had replied when I checked on Nov 7, 2026. Step 2 doesn't depend on the outcome.

**Is my data gone?**
On 4.3.x, no: the saved list of lists is returned, and `{tuple(x) for x in value}` turns it back into the set. Values that were read back as `None` on 4.2.0 aren't recoverable from that return value.

**Does this affect `PostgresSaver` or `RedisSaver`?**
I didn't test them. They're separate packages, and whether they behave the same depends on whether they use the same serializer.

**Will MemorySync fix this error?**
No. It's how LangGraph's checkpointer serializes graph state, which MemorySync doesn't touch. MemorySync is an option for long-term memory across threads, next to your checkpointer.

## Related pages

- [LangGraph memory explained: checkpointer vs store, and per-user memory across threads](https://memorysync.hashnode.dev/langgraph-checkpointer-vs-store-cross-thread-memory)
- [LangGraph AsyncPostgresSaver: fixing 'psycopg.OperationalError: the connection is closed' in FastAPI](https://memorysync.hashnode.dev/langgraph-asyncpostgressaver-connection-is-closed-fastapi)
- [LangGraph SummarizationMiddleware wiped my chat history: keeping the full transcript with PostgresSaver](https://memorysync.hashnode.dev/langgraph-summarization-middleware-full-chat-history)
- [LangGraph: "'list' object has no attribute 'root'" after resume with a Pydantic RootModel in state](https://memorysync.hashnode.dev/langgraph-rootmodel-checkpoint-list-object-has-no-attribute-root)

## Sources

- GitHub issue langchain-ai/langgraph#9516, "Set of tuples in state is restored as a list from a checkpoint" (opened 2026-10-31 21:35 IST, open, 4 comments, no assignee, label external; read via the GitHub API on 2026-11-07)
- GitHub PR langchain-ai/langgraph#9572, "fix(checkpoint): restore tuples when rebuilding sets from msgpack" (opened 2026-11-04 01:33 IST, auto-closed unmerged the same minute; read 2026-11-07)
- PyPI: langgraph 1.2.20 (published 2026-11-05 07:50 IST), 1.2.19 (2026-10-31 03:53 IST), langgraph-checkpoint 4.3.1 (2026-10-27 01:27 IST), 4.3.0 (2026-10-13 03:56 IST) and 4.2.0 (2026-08-08 01:35 IST), langgraph-checkpoint-sqlite 3.1.3
- langgraph-checkpoint source: `libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py` on `main` (read 2026-11-01 and 2026-11-07, unchanged) and in the 4.3.1, 4.3.0 and 4.2.0 packages
- LangGraph docs: [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)
- Offline runs (2026-11-01, Linux, Python 3.13.5, network blocked, no keys; re-run 2026-11-07 on langgraph 1.2.20 with langgraph-checkpoint 4.3.1, Python 3.13.18, same results): Step 1 with `InMemorySaver` and with `SqliteSaver` across two processes; node code after the resume; an old set-of-tuples thread resumed by the Step 2 graph; serializer round trips; Pydantic state; Step 2; Step 3 as two processes; each on langgraph-checkpoint 4.3.1 (langgraph 1.2.19), 4.3.0 (langgraph 1.2.18) and 4.2.0 (langgraph 1.2.14), ormsgpack 1.13.0, pydantic 2.14.2; MemorySync snippet with `langchain-memorysync` 1.2.0, network blocked
- MemorySync LangGraph guide: docs.memorysync.io/guides/langgraph (fetched 2026-11-01)

## Try it

For an overview of MemorySync, see [the MemorySync homepage](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint).

**Next step:** read the [MemorySync LangGraph guide](https://docs.memorysync.io/guides/langgraph?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint), then [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_langgraph-could-not-reconstruct-builtins-set-from-checkpoint). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
