Skip to content

Note

Lightweight nested dict container with path-indexed access.

Constructor

class Note(BaseModel)

All kwargs become top-level content keys. One special case: Note(content={...}) unwraps the dict directly rather than nesting it under a "content" key.

n = Note(a=1, b={"x": 10})          # {"a": 1, "b": {"x": 10}}
n = Note(content={"a": 1, "b": 2})  # {"a": 1, "b": 2}  (unwrapped)
n = Note(content="hello", a=1)      # {"content": "hello", "a": 1}  (no unwrap: 2 kwargs)

Core methods

Method Signature Description
get get(indices, default=UNDEFINED) Nested get; raises KeyError if path missing and no default
set set(indices, value) Nested set; auto-creates intermediate dicts/lists
pop pop(indices, default=UNDEFINED) Remove and return value at path
update update(indices, value) Smart merge at path (see below)
n = Note(a={"b": {"c": 0}}, items=[10, 20])

n.get(("a", "b", "c"))          # 0
n.get(("a", "x"), default=None) # None
n.set(("a", "b", "d"), 99)      # n["a"]["b"] == {"c": 0, "d": 99}
n.pop(("a", "b", "c"))          # returns 0, removes key

update semantics

Existing value Incoming value Behavior
None (missing) list or dict set directly
None (missing) scalar wrap in list, then set
list list extend
list scalar append
dict dict or Note deep_update (recursive merge)
dict scalar raises ValueError
scalar any raises TypeError — use set() to overwrite
n = Note(tags=["a"], meta={"v": 1})
n.update("tags", ["b", "c"])    # tags == ["a", "b", "c"]
n.update("tags", "d")           # tags == ["a", "b", "c", "d"]
n.update("meta", {"w": 2})      # meta == {"v": 1, "w": 2}

Path syntax

indices accepts: str, int, a tuple[str | int, ...], or a list[str | int]. A single string or int is treated as a one-element path. Digit-strings (e.g. "0") are coerced to int when the container is a list.

n["key"]                  # top-level str key
n[("a", "b", 0)]          # tuple path: n.content["a"]["b"][0]
n.get(["a", "b"])         # list path
n.get("0")                # int coerced when container is a list

Flatten / unflatten

n = Note(a={"b": 1, "c": [2, 3]})

flat = n.flatten(sep="|")
# {"a|b": 1, "a|c|0": 2, "a|c|1": 3}

restored = Note.unflatten(flat, sep="|")
# Note(a={"b": 1, "c": {0: 2, 1: 3}})

flatten accepts max_depth: int | None to limit recursion depth. Empty containers ({}, []) are dropped during flatten — round-trip is lossy for those values.

Serialization

data = n.to_dict()                              # deep copy of content, Python mode
data = n.to_dict(mode="json")                   # JSON-safe (enums serialized, models dumped)
data = n.to_dict(exclude_none=True)             # strip None values at all levels
data = n.to_dict(exclude_empty=True)            # strip None + empty containers

n2 = Note.from_dict({"a": 1, "b": 2})          # equivalent to Note(a=1, b=2)
n2 = Note.model_validate({"content": {...}})    # Pydantic validation path
raw = n.model_dump()                            # Pydantic model_dump (wraps in {"content": ...})

Dict-like interface

n = Note(a=1, b={"x": 10})

list(n.keys())               # ["a", "b"]
list(n.keys(flat=True))      # ["a", "b|x"]

list(n.values(flat=True))    # [1, 10]
list(n.items(flat=True))     # [("a", 1), ("b|x", 10)]

len(n)                       # 2  (top-level keys)
"a" in n                     # True  (top-level only)

for key in n:                # iterates top-level keys
    print(key, n[key])

n.clear()                    # empties content

keys(), values(), items() all accept flat=True and a sep kwarg (default "|").

Usage in flows

Session.flow() uses Note internally to accumulate operation context across DAG nodes. Pass a Note as the value argument to update() — it is automatically unwrapped to its content dict before merging.