Editing documents¶
Dotted paths¶
doc["a.b"] = 1 always treats "a.b" as a single literal key.
To descend into nested tables, pass a dotted string (split on .) or any
sequence of literal segments to install or ensure_table:
doc.install("tool.poetry.version", "0.1.0") # [tool.poetry] version = "..."
doc.install(("tool", "weird.key"), 1) # [tool] "weird.key" = 1
ruff = doc.ensure_table("tool.ruff") # creates [tool.ruff] if absent
ruff["line-length"] = 88
Both create any missing intermediate tables on the way down. They differ at the leaf:
install(path, value)writesvalueatpath, replacing whatever was there. It returns the freshly-installed live view (or the leaf value).ensure_table(path)is idempotent: it returns the existing table atpathif there is one, or creates an empty one if not. It never overwrites an existing value.
ensure_table preserves existing tables' form. install does the same for scalar
and inline values. Missing children of inline tables are inline; elsewhere they
are sections:
doc = tomlrt.loads("tool = {}\n")
doc.install("tool.ruff.line-length", 88)
# tool = { ruff = { line-length = 88 } }
When installing a section or array-of-tables from an attached section or document,
install promotes inline ancestors as needed. A scalar or array blocking the
path, or a promotion that would lose inner comments, raises TOMLError without
changing the document. Use promote_inline() to request conversion
without installing a value.
Structural assignment¶
A plain dict value installs as an inline table; a plain list installs as an
inline array.
To pick a different shape, assign a flavoured value:
from tomlrt import AoT, Array, Table
doc["tool"] = Table.section({"version": 1}) # [tool] section
doc["xy"] = Table.inline({"x": 1, "y": 2}) # xy = { x = 1, y = 2 }
doc["pkgs"] = AoT([{"a": 1}, {"b": 2}]) # [[pkgs]] … [[pkgs]]
doc["tags"] = Array(["a", "b"], multiline=True) # multi-line array
Live vs snapshot¶
Assigning a fresh Table.section(...), Table.inline(...), Array(...), or
AoT(...) attaches it live: your reference becomes the live view at the
destination, and later mutations through that reference show up in the document.
xs = Array([1, 2])
doc["xs"] = xs
xs.append(3) # doc["xs"] is now [1, 2, 3]
assert doc["xs"] is xs
t = Table.section()
doc["a"] = t
t["x"] = 1 # doc["a"] is now {"x": 1}
assert doc["a"] is t
Plain dict / list values are snapshot on assignment — mutating the
original after assignment does not affect the document.
Reach for Table.section, Table.inline, or Array when you want live
semantics.
A container that is already attached somewhere is deep-cloned on assignment, so
two slots never share state.
This applies whether the source and destination are in the same document
(doc["b"] = doc["a"]) or different ones (d2["x"] = d1["x"]).
The clone is byte-faithful: comments, whitespace, and string / number style on
the bytes you didn't touch survive the move.
That holds when the two overlap, too — doc["a"]["b"] = doc["a"] copies the
a that was there before the assignment began.
Assigning a whole parsed Document as a value lifts its body into a section,
preserving the comments and layout that were in the source:
template = tomlrt.loads(template_text) # a standalone file, no [header]
doc["tool"]["bumpversion"] = template # becomes [tool.bumpversion], trivia intact
The document's own file-level preamble / epilogue (comment blocks separated from the body by a blank line) belong to no key and are not carried across.
Removal and orphaning¶
Removing a Table, Array, or AoT — via del, pop, clear, or overwrite
— detaches the view.
It keeps its data, but further mutations no longer reach the document:
old = doc.pop("tool") # detached Table view
old["debug"] = True # does NOT affect doc
Reattaching the detached view keeps its existing entry and nested-view references live at the new location.
Copying or moving a header-less table preserves the order and spelling of its dotted keys. Those keys remain in the enclosing section's body; child sections and arrays-of-tables remain structural blocks below it. If the table has only child headers, both copying and moving use normal section placement rather than inserting it ahead of existing sections.
Arrays-of-tables¶
AoT.add() appends a fresh entry and returns the new Table view, so you can
keep mutating it:
pkgs = doc.aot("packages")
entry = pkgs.add({"name": "foo"})
entry["version"] = "1.0"
An array-of-tables with no entries has no [[key]] syntax, so it serialises as
key = [] — which re-parses as an empty inline Array, not an AoT.
Integer assignment and any slice assignment replacing the same number of entries operate in place: destination headers (including comments), held entry views and interleaved sections stay put. Only a slice that changes the array's length gathers the entries into their new order. Parsed sections and array-of-tables entries both contribute their original body formatting; an in-place replacement keeps the destination's header.
Overlapping sources are captured before changing bodies or array membership, so
pkgs[::-1] = pkgs exchanges the bodies, and
pkgs[0] = {"nested": pkgs[0]} copies the original body into a nested section.
Likewise, pkgs[:0] = [{"nested": pkgs}] copies the array before inserting into it.
This also works through nested mappings, lists and factory inputs. Fresh typed
values still attach at their first installed occurrence; adopting an orphan
child still removes it from its old parent.
Reshaping the layout¶
Editing changes a document's data. To reshape its layout — sort keys, switch inline values between single- and multi-line, promote an inline value to a section, or snap a subtree to a canonical format — see Layout.