What Is TOML?
TOML — Tom's Obvious, Minimal Language — is a configuration file format created by Tom Preston-Werner (co-founder of GitHub) in 2013. Its design goal is to be a minimal format that maps unambiguously to a hash table — a configuration file that is obvious to read and to write, with explicit typing and clear semantics that eliminate the surprising behaviors found in YAML and INI files.
TOML reached version 1.0.0 in January 2021 after years of refinement and community consensus. It is now the default configuration format for the Rust ecosystem (Cargo.toml), used by Python packaging tools (pyproject.toml), and adopted by many Go, PHP, and JavaScript projects.
Design Philosophy
TOML was built on specific design constraints:
- Obvious: a human reading a TOML file should immediately understand the structure.
- Minimal: only the features necessary for configuration, nothing more.
- Unambiguous: every TOML file maps to exactly one data structure with no type coercion surprises.
- Compatible with hash tables: designed for 1:1 mapping to key-value stores.
TOML deliberately avoids YAML's complexity (200+ page spec) and JSON's lack of comments. It is simpler than XML while being more structured than INI files.
Basic Syntax
# This is a TOML comment
# Key-value pairs (top-level table)
name = "KaijuConverter"
version = "2.1.0"
active = true
price = 9.99
max_connections = 100
# Dates and times (RFC 3339)
created_at = 2024-01-15T10:30:00Z
local_date = 2024-01-15 # date without time
local_time = 10:30:00 # time without date
# Multi-line basic string (backslash-escaped)
description = """
This is a
multi-line string.
"""
# Multi-line literal string (no escaping)
regex = '''
^[a-z]+$
'''
Tables (Objects)
Tables are the TOML equivalent of objects/dictionaries:
[database]
host = "localhost"
port = 5432
name = "kaijudb"
ssl = true
[database.pool]
min = 2
max = 20
timeout = 30
[server]
host = "0.0.0.0"
port = 8080
This maps to:
{
"database": {
"host": "localhost",
"port": 5432,
"name": "kaijudb",
"ssl": true,
"pool": { "min": 2, "max": 20, "timeout": 30 }
},
"server": { "host": "0.0.0.0", "port": 8080 }
}
Dotted keys provide an inline shorthand for nested tables:
database.host = "localhost"
database.port = 5432
Arrays of Tables
[[double_brackets]] create arrays of tables — the TOML way to represent a list of objects:
[[servers]]
name = "web-01"
ip = "192.168.1.10"
role = "primary"
[[servers]]
name = "web-02"
ip = "192.168.1.11"
role = "replica"
[[servers]]
name = "web-03"
ip = "192.168.1.12"
role = "replica"
This maps to an array of server objects, equivalent to:
{
"servers": [
{"name": "web-01", "ip": "192.168.1.10", "role": "primary"},
{"name": "web-02", "ip": "192.168.1.11", "role": "replica"},
{"name": "web-03", "ip": "192.168.1.12", "role": "replica"}
]
}
Data Types
TOML has explicit, unambiguous types:
| Type | Example | Notes |
|---|---|---|
| String | "hello" or 'raw' |
Basic (escaped) or literal (no escape) |
| Integer | 42, -7, 1_000_000 |
Underscores for readability |
| Float | 3.14, 1.0e10, inf, nan |
IEEE 754 double |
| Boolean | true, false |
Only lowercase |
| Datetime | 2024-01-15T10:30:00Z |
RFC 3339 full; offset, local-datetime, local-date, local-time |
| Array | [1, 2, 3] or ["a", "b"] |
Mixed types allowed in 1.0 but not recommended |
| Inline table | {x = 1, y = 2} |
Single-line only; no trailing comma |
| Table | [section] |
Block definition |
| Array of tables | [[section]] |
Repeatable block |
String types:
- Basic string (
"...") — supports escape sequences:\n,\t,\uXXXX,\\,\". - Literal string (
'...') — no escaping; what you write is what you get. - Multi-line basic (
"""...""") — leading newline after opening quotes is trimmed. - Multi-line literal (
'''...''') — no escaping, raw content.
TOML in Practice: Cargo.toml
The Rust package manifest is the most widely used TOML file in the world:
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
authors = ["Alice <alice@example.com>"]
description = "A fast file converter"
license = "MIT"
repository = "https://github.com/alice/my-app"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.11", default-features = false, features = ["json"] }
[dev-dependencies]
mockall = "0.12"
[profile.release]
opt-level = 3
lto = true
strip = true
TOML in Python Packaging: pyproject.toml
PEP 518 and PEP 621 standardized pyproject.toml as the Python project descriptor:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
"requests>=2.28",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "mypy", "ruff"]
[tool.ruff]
line-length = 88
select = ["E", "F", "I"]
[tool.mypy]
strict = true
TOML vs. YAML vs. INI vs. JSON
| Feature | TOML | YAML | INI | JSON |
|---|---|---|---|---|
| Comments | Yes | Yes | Yes | No |
| Explicit types | Yes | Partial | No | Yes |
| Multi-line strings | Yes | Yes | No | Escape only |
| Nested structures | Yes (tables) | Yes | Limited (sections) | Yes |
| Arrays of objects | Yes ([[]]) |
Yes | No | Yes |
| Dates/times | Native | Coerced | No | No |
| Anchors/aliases | No | Yes | No | No |
| Schema standard | No | No | No | JSON Schema |
| Spec complexity | Low | Very high | None | Low |
| Ambiguity | None | High | High | None |
| Best for | Config files | CI/CD, K8s | Simple settings | APIs, data |
TOML wins on explicitness and unambiguity. YAML wins on expressiveness and ecosystem breadth for DevOps. JSON wins for API interchange. INI is only acceptable for legacy compatibility.
Parsing TOML
Python:
import tomllib # stdlib since Python 3.11
with open('config.toml', 'rb') as f:
config = tomllib.load(f)
# For writing, use tomli-w (third-party)
import tomli_w
with open('output.toml', 'wb') as f:
tomli_w.dump(data, f)
Rust (with serde):
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
name: String,
version: String,
database: DatabaseConfig,
}
let config: Config = toml::from_str(toml_str)?;
Go:
import "github.com/BurntSushi/toml"
var config Config
_, err := toml.Decode(tomlString, &config)
JavaScript/Node.js:
import { parse } from '@iarna/toml';
const config = parse(tomlString);
Converting TOML
- TOML → JSON:
yq e -o=json config.toml, Pythonjson.dumps(tomllib.loads(s)). - TOML → YAML:
yq e -o=yaml config.toml. - JSON → TOML:
yq e -o=toml input.json. - YAML → TOML:
yq e -o=toml input.yaml.
Best Practices
- Use TOML for configuration that humans edit — its explicit types eliminate surprises.
- Prefer
[table]headers over deeply nested inline tables for readability. - Use
[[array of tables]]for lists of objects — cleaner than YAML sequences of mappings. - Always use RFC 3339 datetimes — TOML's native date/time types prevent ambiguity.
- Use literal strings (
'...') for regex patterns, file paths, and any string with backslashes. - Use underscores in numbers for readability:
1_000_000instead of1000000. - Validate schema with
taplo(Rust-based TOML toolkit with JSON Schema integration). - Sort keys alphabetically within tables for consistent diffs.
- Do not mix table definition and key-value assignment in different places.
- Prefer TOML over YAML for new projects where CI/CD YAML idioms are not required.
Related conversions
Frequent conversions across the catalogue: