TOML Cheatsheet
Comparison to JSON and YAML
Use this TOML reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The feature matrix
| Feature | TOML 1.0.0 | JSON | YAML 1.2 |
|---|---|---|---|
| comments | # | none | # |
| multi-line strings | """ and ''' | "a\nb" only | | and > blocks |
| trailing commas | arrays only | forbidden | allowed |
| unquoted keys | yes (bare keys) | no | yes |
| references / anchors | no | no | &a / *a |
| inheritance | no | no | <<: *base |
| dates as a type | 4 types | string only | 1.1 timestamp |
| null type | none | null | null / ~ / empty |
| heterogeneous arrays | yes | yes | yes |
| non-string keys | no | no | yes |
| indentation significant | no | no | yes |
| several docs per file | no | no | --- |
| duplicate keys | error | last wins (varies) | varies by parser |
| implicit type coercion | none | none | heavy |
| spec length | ~1,200 lines | ~15 pages (RFC 8259) | ~80 pages |
| streaming friendly | no | yes | yes |
| in Python stdlib | tomllib (3.11+) | json | no, needs PyYAML |
Ambiguity is the real difference
TOML and JSON have no type coercion. A value's type is determined entirely by its syntax, and there is no set of characters that means one thing to one parser and another to the next.
# YAML: every one of these is a trap country: NO # false under a 1.1 parser mode: 0644 # 420 under a 1.1 parser version: 1.10 # the float 1.1 time: 12:30 # 750 under a 1.1 parser
# TOML: syntax decides, nothing infers country = "NO" # a string, always mode = 0o644 # 420, explicitly octal version = "1.10" # a string, always time = 12:30:00 # a local time, seconds required
TOML also has no duplicate-key ambiguity. Redefining a key or a table is an error, so a config never silently loses a value. YAML's answer varies by parser, and PyYAML's answer is to keep the last one silently.
Where each one wins
| Situation | Pick |
|---|---|
| a hand-edited config with a flat-ish shape | TOML |
| deeply nested config (5+ levels) | YAML |
| repeated blocks that should share a base | YAML, for <<: *base |
| an API request or response body | JSON |
| machine-to-machine, anything untrusted | JSON |
| a file a program reads and rewrites | JSON, no comments to lose |
| a project manifest or tool config | TOML |
| Kubernetes or CI pipelines | YAML, that is what the tools take |
| logs, one record per line | NDJSON |
TOML's design goal is a config file that is obvious to a human reading it without the spec open. That trade shows up as verbosity once you nest deeply, which is the one genuine argument for YAML.
The nesting cost
TOML's headers stop paying off around four levels. Compare a Kubernetes-shaped document:
spec:
template:
spec:
containers:
- name: api
image: api:1.2
resources:
limits:
cpu: 500m[[spec.template.spec.containers]] name = "api" image = "api:1.2" [spec.template.spec.containers.resources.limits] cpu = "500m"
The TOML is valid and produces the same structure, but the second header repeats six
segments and gives no visual clue that it belongs to the container above it. This is
exactly why Kubernetes is YAML and pyproject.toml is TOML.
Who uses what
| Ecosystem | Format |
|---|---|
Rust (Cargo.toml, rustfmt.toml) | TOML |
Python packaging (pyproject.toml, uv, ruff) | TOML |
Go modules (go.mod) | its own format |
Node (package.json, tsconfig.json) | JSON / JSONC |
| Kubernetes, Helm, Ansible | YAML |
| GitHub Actions, GitLab CI, CircleCI | YAML |
| Docker Compose | YAML |
| Netlify, Fly.io, Traefik, Hugo | TOML (Hugo takes all three) |
| Terraform | HCL |
Python packaging moving to TOML in PEP 518 and
PEP 621 is the single biggest reason TOML
matters today. tomllib landed in the standard library in Python 3.11.
Reading and converting
| Language | Read TOML | Write TOML |
|---|---|---|
| Python | tomllib (3.11+), tomli | tomli-w, tomlkit (keeps comments) |
| Rust | toml, serde | toml |
| Go | github.com/BurntSushi/toml | same |
| Node | smol-toml, @iarna/toml | @iarna/toml |
| CLI | taplo get, yq -p toml | taplo fmt, yq -o toml |
import tomllib with open("pyproject.toml", "rb") as f: # note "rb", bytes not text cfg = tomllib.load(f) print(cfg["project"]["name"])
# TOML -> JSON yq -p toml -o json '.' pyproject.toml python -c "import tomllib,json,sys; \ print(json.dumps(tomllib.load(sys.stdin.buffer), default=str))" < pyproject.toml
tomllib.load requires a binary file object. Passing a text-mode file raises
TypeError: File must be opened in binary mode. tomllib.loads takes a str, and
the standard library ships no TOML writer, so use tomli-w for plain output or
tomlkit when you need to edit a file and preserve its comments and formatting.