TOML Cheatsheet
Tables
Use this TOML reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
A header owns everything until the next header
[name] starts a table. Every key = value after it belongs to that table until
the next [header] or [[header]] line or end of file.
title = "top level" [server] host = "localhost" port = 8080 [database] url = "postgres://localhost/app" pool = 10
parses to:
{"title": "top level",
"server": {"host": "localhost", "port": 8080},
"database": {"url": "postgres://localhost/app", "pool": 10}}Note that title is at the root because it appears before any header. Anything
after [server] is inside server, with no way to get back to the root except a
dotted key from a later header. In practice: put every root-level key at the top
of the file, before the first [.
Nesting
Dots in a header nest tables. Whitespace around the dots is allowed and ignored.
[a.b.c] x = 1 [ a . b . d ] y = 2
gives {"a": {"b": {"c": {"x": 1}, "d": {"y": 2}}}}. Header names follow the key
rules exactly, so quoting works for odd segments:
["ʞ.ǝʎ"] x = 1 [tool."ruff-extra"] y = 2 [servers."192.168.1.1"] role = "primary"
["ʞ.ǝʎ"] is one table whose name contains a dot. [servers."192.168.1.1"]
is servers containing a table named 192.168.1.1.
Super-tables are created implicitly
You never have to declare a parent. Writing [a.b.c] creates a and a.b on the
way.
[fruit.apple.texture]
smooth = truegives {"fruit": {"apple": {"texture": {"smooth": true}}}}. And an implicitly
created parent may be defined later, which is legal and common:
[x.y.z.w] a = 1 [x] b = 2
gives {"x": {"y": {"z": {"w": {"a": 1}}}, "b": 2}}. Ordering is free, so a
file may declare children before parents. Most tools write parents first because it
reads better, not because the spec requires it.
You cannot redefine a table
This is the rule that separates TOML from YAML, and the one that produces most error messages.
| Attempt | Result |
|---|---|
[a] twice | error, Cannot declare ('a',) twice |
[a.b] twice | error, Cannot declare ('a', 'b') twice |
[a.b] then [a] | fine, a was only implicit |
a.b = 1 in [t] then [t.a] | error, a is already a defined table |
a = 1 then [a] | error, Cannot overwrite a value |
p = [] then [[p]] | error, Cannot mutate immutable namespace |
[[p]] then [p] | error, Cannot declare ('p',) twice |
[fruit] apple = "red" [fruit] banana = "yellow"
fails with TOMLDecodeError: Cannot declare ('fruit',) twice (at line 4, column 7).
The fix is to merge the two blocks, or to nest: [fruit.apple] and
[fruit.banana].
The practical consequence for generated files is that a tool appending a
[tool.something] block must first check whether one already exists. tomlkit
handles this. A naive open(f, "a") does not.
Dotted keys versus headers
A dotted key defines the same structure as a header. Pick one style per table and stay with it, because mixing them is where the redefinition errors come from.
# these two files are identical [tool.ruff] line-length = 100 target-version = "py312"
tool.ruff.line-length = 100 tool.ruff.target-version = "py312"
Both give {"tool": {"ruff": {"line-length": 100, "target-version": "py312"}}}.
The header form wins for more than two keys, and the dotted form wins for a single
setting you do not want a whole block for.
A dotted key inside a table defines a sub-table you then own. After
[t]
a.b = 1[t.a] is a redefinition and fails with Cannot declare ('t', 'a') twice. This is
the trap: a one-line tool.ruff.lint.select = ["E"] at the top of a
pyproject.toml makes a later [tool.ruff.lint] block illegal, with
Cannot declare ('tool', 'ruff', 'lint') twice.
Empty tables and ordering
[empty] [also-empty] # just a comment [filled] x = 1
gives {"empty": {}, "also-empty": {}, "filled": {"x": 1}}. A header with nothing
under it is a real, empty table, which is how you express "this feature is enabled
with all defaults".
Key order within a table is not semantically meaningful, and most parsers do not
preserve it (Python's tomllib returns a plain dict, so insertion order happens
to survive). Table order in the file is likewise free. Do not encode meaning in
either.