The Complete Guide to TOML Configuration Files
Configuration files are the backbone of modern software. Every build tool, package manager, and deployment platform needs a way to read structured settings from a file. For decades, developers reached for JSON, YAML, or INI files, each with significant tradeoffs. TOML was created to offer something better: a configuration format that is easy for humans to read and write, unambiguous to parse, and maps cleanly to a hash table.
This guide covers everything you need to know about TOML, from basic syntax to advanced features like arrays of tables. Whether you are writing your first Cargo.toml, setting up pyproject.toml, or evaluating TOML for your own project, you will leave with a thorough understanding of the format and the confidence to use it effectively.
What Is TOML and Why Was It Created?
TOML stands for Tom's Obvious, Minimal Language, created by Tom Preston-Werner (co-founder of GitHub) in 2013. The project's goal is stated plainly in its specification: "A config file format for humans." Version 1.0.0 was released in January 2021, marking the format as stable and ready for production use.
TOML was born out of frustration with the existing options. Each established format has drawbacks that become painful when used for configuration:
JSON is ubiquitous and unambiguous, but it was designed for data interchange between machines, not for humans to edit by hand. It lacks comments, requires quoted keys, demands trailing-comma discipline, and has no native date type. Editing a large JSON configuration file is tedious and error-prone.
YAML is more human-friendly than JSON, but it is notoriously complex. The YAML specification is over 80 pages long. Indentation-sensitive syntax leads to subtle bugs. The "Norway problem" (where NO is parsed as a boolean false) and implicit type coercion have bitten countless developers. A YAML file that looks correct can parse into something completely unexpected.
INI files are simple and readable, but they have no formal specification. Different parsers handle sections, nesting, data types, and escaping differently. There is no standard way to represent arrays, nested structures, or typed values.
TOML takes the readability of INI files, adds well-defined data types and nesting, and keeps the specification short enough that a developer can read it in one sitting. The result is a format where what you see is what you get, there are no surprises during parsing, and comments are a first-class feature.
TOML Syntax Basics
A TOML file is a sequence of key/value pairs, optionally organized into tables (sections). Every TOML file maps to a hash table (dictionary/object). Here are the fundamental building blocks.
Comments
Comments start with a hash symbol and run to the end of the line. They can appear on their own line or after a value:
# This is a full-line comment
name = "DevToolbox" # This is an inline comment
Unlike JSON, TOML treats comments as essential. Configuration files are read by humans who need context about why a setting exists and what happens when you change it.
Strings
TOML provides four types of strings to handle different needs:
# Basic strings use double quotes and support escape sequences
title = "TOML Configuration Guide"
path = "C:\\Users\\dev\\config"
greeting = "Hello,\nWorld!"
# Multi-line basic strings use triple double quotes
description = """
This is a multi-line string.
It preserves line breaks and supports \
line ending backslash (trims whitespace on next line)."""
# Literal strings use single quotes — no escaping
windows_path = 'C:\Users\dev\config'
regex = '\d+\.\d+'
# Multi-line literal strings use triple single quotes
raw_text = '''
No escape sequences here.
Backslashes are just backslashes: \n \t \\
Everything is taken literally.'''
The four string types cover every scenario: basic strings for most values, literal strings when you need backslashes without escaping (file paths, regex patterns), and multi-line variants of each for longer content.
Integers and Floats
# Integers
port = 8080
negative = -42
hex_color = 0xDEADBEEF
octal_permissions = 0o755
binary_flags = 0b11010110
# Underscores for readability (ignored by parsers)
large_number = 1_000_000
hex_value = 0xFF_FF
# Floats
pi = 3.14159
negative_float = -0.01
scientific = 5e+22
small = 6.626e-34
# Special float values
infinity = inf
negative_infinity = -inf
not_a_number = nan
TOML integers are 64-bit signed, giving a range from -2^63 to 2^63-1. The underscore separator is a small but valuable feature for configuration files, where you often encounter large numbers like timeout values in milliseconds or file size limits in bytes.
Booleans
# Booleans are always lowercase
debug = true
verbose = false
Unlike YAML, there is no ambiguity. Only true and false are booleans. The strings "yes", "no", "on", "off" are just strings. This eliminates an entire category of configuration bugs.
Dates and Times
TOML has first-class date and time support following RFC 3339:
# Offset Date-Time (full timestamp with timezone)
created_at = 2026-02-11T09:30:00Z
updated_at = 2026-02-11T14:30:00+05:00
# Local Date-Time (no timezone — represents a wall clock time)
meeting = 2026-03-15T10:00:00
# Local Date
birthday = 1990-05-20
# Local Time
alarm = 07:30:00
Native date support is a significant advantage over JSON and INI. In JSON, dates are just strings, and every application has to agree on a format and parse them manually. In TOML, the parser hands you an actual date object.
Tables: Structuring Your Configuration
Tables are the primary way to organize keys in TOML. They correspond to hash tables (objects/dictionaries) in the resulting data structure.
Standard Tables
# Keys before any table header belong to the root table
title = "My Application"
# [server] creates a table named "server"
[server]
host = "0.0.0.0"
port = 8080
workers = 4
# [database] creates a sibling table
[database]
host = "localhost"
port = 5432
name = "myapp"
pool_size = 10
This parses into a structure equivalent to:
{
"title": "My Application",
"server": {
"host": "0.0.0.0",
"port": 8080,
"workers": 4
},
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp",
"pool_size": 10
}
}
Nested Tables with Dotted Keys
You can create nested structures using dotted keys or nested table headers:
# Using dotted table headers
[server.tls]
cert = "/etc/ssl/cert.pem"
key = "/etc/ssl/key.pem"
# Using dotted keys (equivalent to creating sub-tables)
[database]
connection.timeout = 30
connection.retries = 3
connection.backoff = 1.5
# The above is equivalent to:
# [database.connection]
# timeout = 30
# retries = 3
# backoff = 1.5
Inline Tables
For small, self-contained groupings, inline tables keep everything on one line:
# Inline tables are compact single-line tables
point = { x = 1, y = 2 }
color = { r = 255, g = 128, b = 0 }
# Useful for simple nested structures
[servers]
alpha = { ip = "10.0.0.1", role = "frontend" }
beta = { ip = "10.0.0.2", role = "backend" }
gamma = { ip = "10.0.0.3", role = "database" }
An important constraint: inline tables must be entirely on one line and cannot be extended after definition. You cannot add keys to an inline table later in the file. This is a deliberate design choice to keep inline tables simple and unambiguous.
Arrays and Arrays of Tables
Basic Arrays
# Arrays of simple values
ports = [8080, 8081, 8082]
hosts = ["alpha.example.com", "beta.example.com"]
flags = [true, false, true]
# Mixed-type arrays (allowed since TOML v1.0)
mixed = ["string", 42, true, 2026-02-11]
# Multi-line arrays (trailing comma allowed)
dependencies = [
"serde",
"tokio",
"reqwest",
"tracing",
]
TOML arrays can span multiple lines and allow a trailing comma after the last element. This is a small ergonomic win that eliminates diff noise when adding items to a list in version control.
Arrays of Tables
This is the most powerful feature in TOML and the one most newcomers find confusing at first. Double brackets [[...]] define an array of tables, which is an array where each element is a hash table:
# Each [[products]] creates a new element in the "products" array
[[products]]
name = "Widget"
price = 9.99
sku = "WDG-001"
[[products]]
name = "Gadget"
price = 24.99
sku = "GDG-002"
[[products]]
name = "Doohickey"
price = 4.99
sku = "DHK-003"
This parses into:
{
"products": [
{ "name": "Widget", "price": 9.99, "sku": "WDG-001" },
{ "name": "Gadget", "price": 24.99, "sku": "GDG-002" },
{ "name": "Doohickey", "price": 4.99, "sku": "DHK-003" }
]
}
Arrays of tables can be nested. Here is a more complex example showing packages with multiple dependencies:
[[packages]]
name = "web-server"
version = "1.2.0"
[[packages.dependencies]]
name = "tokio"
version = "1.36"
features = ["full"]
[[packages.dependencies]]
name = "axum"
version = "0.7"
[[packages]]
name = "cli-tool"
version = "0.5.0"
[[packages.dependencies]]
name = "clap"
version = "4.5"
features = ["derive"]
Each [[packages]] starts a new package, and each [[packages.dependencies]] adds a dependency to the most recently defined package. This nesting pattern is used extensively in real-world TOML files.
Real-World TOML: Common Use Cases
TOML has been adopted by major tools across multiple ecosystems. Understanding these real-world patterns will help you work with TOML files you encounter in practice.
Rust: Cargo.toml
Cargo, the Rust package manager, was one of the earliest high-profile adopters of TOML. Every Rust project has a Cargo.toml at its root:
[package]
name = "my-web-app"
version = "0.1.0"
edition = "2021"
authors = ["Alice Dev <alice@example.com>"]
description = "A fast web application"
license = "MIT"
repository = "https://github.com/alice/my-web-app"
[dependencies]
axum = "0.7"
tokio = { version = "1.36", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tracing = "0.1"
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
[dev-dependencies]
reqwest = { version = "0.12", features = ["json"] }
tower = { version = "0.4", features = ["util"] }
[profile.release]
opt-level = 3
lto = true
strip = true
The Cargo.toml format makes heavy use of inline tables for dependency specifications and standard tables for grouping related settings. The [profile.release] section demonstrates dotted table headers for nested configuration.
Python: pyproject.toml
Python's packaging ecosystem has converged on pyproject.toml (PEP 517/518/621) as the standard project configuration file, replacing setup.py and setup.cfg:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "2.1.0"
description = "A useful Python package"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Alice Dev", email = "alice@example.com" },
]
dependencies = [
"requests>=2.28",
"click>=8.0",
"rich>=13.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"ruff>=0.3",
"mypy>=1.8",
]
[project.scripts]
my-cli = "my_package.cli:main"
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-v --tb=short"
Notice the [tool.*] convention. TOML's hierarchical tables make it natural for multiple tools to share a single configuration file without conflicts. Ruff, pytest, mypy, black, and many other Python tools all read their settings from pyproject.toml under their own [tool.name] namespace.
Hugo: hugo.toml
Hugo, the popular static site generator, uses TOML as its default configuration format:
baseURL = "https://example.com/"
languageCode = "en-us"
title = "My Tech Blog"
theme = "papermod"
paginate = 10
[params]
author = "Alice Dev"
description = "Writing about software engineering"
showReadingTime = true
showShareButtons = true
showCodeCopyButtons = true
[params.homeInfoParams]
title = "Welcome"
content = "A blog about building software"
[[params.socialIcons]]
name = "github"
url = "https://github.com/alice"
[[params.socialIcons]]
name = "twitter"
url = "https://twitter.com/alice"
[menu]
[[menu.main]]
name = "Posts"
url = "/posts/"
weight = 1
[[menu.main]]
name = "About"
url = "/about/"
weight = 2
[[menu.main]]
name = "Tags"
url = "/tags/"
weight = 3
[markup.highlight]
style = "dracula"
lineNos = true
Netlify: netlify.toml
Netlify uses TOML to configure build settings, redirects, and headers:
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
NPM_FLAGS = "--legacy-peer-deps"
[context.production.environment]
API_URL = "https://api.example.com"
[context.deploy-preview.environment]
API_URL = "https://staging-api.example.com"
# Redirect rules
[[redirects]]
from = "/blog/*"
to = "/posts/:splat"
status = 301
[[redirects]]
from = "/api/*"
to = "https://api.example.com/:splat"
status = 200
force = true
# Security headers
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
The Netlify example shows how arrays of tables ([[redirects]], [[headers]]) excel at representing lists of configuration records, each with multiple fields.
TOML vs YAML vs JSON: Detailed Comparison
Choosing a configuration format depends on your specific needs. Here is a detailed comparison across the dimensions that matter most for configuration files.
| Feature | TOML | YAML | JSON |
|---|---|---|---|
| Comments | Yes (#) | Yes (#) | No |
| Native date type | Yes (RFC 3339) | Partial (implicit) | No |
| Spec complexity | Short (~3,000 words) | Very long (80+ pages) | Short (RFC 8259) |
| Trailing commas | Yes (in arrays) | N/A (no commas) | No |
| Indentation-sensitive | No | Yes | No |
| Implicit type coercion | No | Yes (major gotcha) | No |
| Multi-document | No | Yes (---) | No |
| Anchors/references | No | Yes (& and *) | No |
| Deeply nested data | Verbose at 3+ levels | Natural via indentation | Natural via nesting |
| API data interchange | Not designed for it | Possible but unusual | Industry standard |
| Best for | Configuration files | Complex config, CI/CD | Data exchange, APIs |
When to choose TOML: Configuration files with moderate nesting (1-3 levels deep), where humans will read and edit the file frequently. Package manifests, application settings, tool configuration.
When to choose YAML: Complex configuration with deep nesting (Kubernetes manifests, CI/CD pipelines, Ansible playbooks) or when you need features like anchors to reduce repetition. Accept the complexity tradeoff in exchange for expressiveness.
When to choose JSON: Data interchange between services, API responses, or any context where the file is primarily machine-generated and machine-consumed. If humans rarely edit the file, JSON's lack of comments and verbosity are not drawbacks.
Common Mistakes and Gotchas
Even though TOML is simpler than YAML, there are several pitfalls that catch people regularly. Understanding these upfront will save you debugging time.
1. Redefining Keys
TOML does not allow defining the same key twice. This is an error, not a silent override:
# ERROR: duplicate key
name = "first"
name = "second" # Parser will reject this
# Also an error across table and dotted keys:
[server]
host = "localhost"
[server] # ERROR: table [server] already defined
port = 8080
This is actually a safety feature. In YAML and many INI parsers, duplicate keys silently use the last value, which can lead to subtle bugs when different parts of a large configuration file unknowingly set the same key.
2. Confusing Table Syntax with Array of Tables Syntax
# This creates a SINGLE table
[server]
host = "alpha"
# This creates an ARRAY of tables (list of servers)
[[server]]
host = "alpha"
[[server]]
host = "beta"
# You cannot mix them — this is an error:
[server]
host = "alpha"
[[server]] # ERROR: cannot define array of tables; [server] already exists as a table
host = "beta"
3. Inline Tables Cannot Be Extended
# Inline tables are immutable after definition
point = { x = 1, y = 2 }
point.z = 3 # ERROR: cannot add keys to an inline table
# Instead, use a standard table if you need to add keys later
[point]
x = 1
y = 2
z = 3
4. Strings Must Be Explicitly Quoted
# Unlike YAML, bare values are never strings
# TOML is strict about types:
count = 42 # This is an integer
count = "42" # This is a string
# There is no "bare string" — unquoted values must be
# a recognized type (bool, int, float, date)
name = hello # ERROR: not a valid value
name = "hello" # Correct
5. Table Ordering Matters for Arrays of Tables
# Keys after [[array]] belong to the LAST element in that array
[[fruits]]
name = "apple"
[fruits.details] # This belongs to the apple, not a new fruit
color = "red"
weight = 150
[[fruits]]
name = "banana"
[fruits.details] # This belongs to the banana
color = "yellow"
weight = 120
6. No Null Value
TOML has no null, nil, or None type. If a key has no value, omit it entirely. This is a deliberate design decision: a configuration file should express what is configured, not what is absent. Your application's default values handle the "not configured" case.
7. Quoted Keys for Special Characters
# Bare keys can only contain A-Za-z0-9, dashes, and underscores
server-name = "alpha" # Valid bare key
server_port = 8080 # Valid bare key
# Use quotes for anything else
"127.0.0.1" = "localhost"
"content-type" = "text/html"
"ç" = "cedilla"
"" = "blank" # Empty string key is valid
Validation and Tooling
A robust tooling ecosystem has grown around TOML. Here are the most useful tools and libraries for working with TOML files in production.
Command-Line Validators
# taplo — the most popular TOML toolkit (Rust-based)
# Validates, formats, and lints TOML files
cargo install taplo-cli
# Validate a file
taplo check config.toml
# Format a file (like prettier for TOML)
taplo format config.toml
# Lint with schema validation
taplo lint config.toml
# Python's built-in tomllib (Python 3.11+)
python3 -c "import tomllib; tomllib.loads(open('config.toml','rb').read())"
# Node.js validation with @iarna/toml
npx -y toml-cli < config.toml
Editor Support
All major editors have excellent TOML support:
- VS Code: The "Even Better TOML" extension (taplo) provides syntax highlighting, validation, formatting, schema-driven autocompletion, and hover documentation. It is the gold standard for TOML editing.
- JetBrains IDEs: Built-in TOML support with syntax highlighting and basic validation. The TOML plugin adds schema support.
- Vim/Neovim: Tree-sitter grammar for TOML provides fast, accurate syntax highlighting. The
vim-tomlplugin adds format-on-save support. - Sublime Text: The TOML package provides syntax highlighting and snippets.
Language Libraries
Every major programming language has mature TOML parsing libraries:
# Python (3.11+ has tomllib in stdlib)
import tomllib
with open("config.toml", "rb") as f:
config = tomllib.load(f)
# For writing TOML, use tomli-w
import tomli_w
with open("config.toml", "wb") as f:
tomli_w.dump({"server": {"port": 8080}}, f)
// Rust — toml crate with serde
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
server: ServerConfig,
}
#[derive(Deserialize)]
struct ServerConfig {
host: String,
port: u16,
}
let config: Config = toml::from_str(&std::fs::read_to_string("config.toml")?)?;
// Go — BurntSushi/toml (by TOML spec co-author)
import "github.com/BurntSushi/toml"
type Config struct {
Server struct {
Host string
Port int
}
}
var config Config
_, err := toml.DecodeFile("config.toml", &config)
// JavaScript/Node.js — @iarna/toml or smol-toml
import { parse } from "smol-toml";
import { readFileSync } from "fs";
const config = parse(readFileSync("config.toml", "utf-8"));
JSON Schema for TOML
The taplo toolkit supports JSON Schema validation for TOML files. Many popular TOML formats already have published schemas:
# taplo.toml — configure schema associations
[[rule]]
keys = ["Cargo.toml"]
[rule.schema]
url = "https://json.schemastore.org/cargo"
[[rule]]
keys = ["pyproject.toml"]
[rule.schema]
url = "https://json.schemastore.org/pyproject"
Schema validation catches errors before they reach your application: misspelled keys, wrong value types, missing required fields. It is especially valuable for large configuration files where a typo might not surface until deployment.
Best Practices for TOML Configuration Files
Based on patterns from widely-used TOML files across the Rust, Python, and Go ecosystems, here are practices that lead to maintainable configuration:
- Group related settings into tables. Do not dump everything into the root table. Use
[server],[database],[logging]sections to organize your configuration logically. - Comment every non-obvious setting. TOML supports comments for a reason. Explain units (seconds? milliseconds?), valid ranges, and the consequences of changing a value.
- Use inline tables sparingly. Reserve them for simple key-value pairs that fit comfortably on one line. If you are squinting to read the line, use a standard table instead.
- Prefer standard tables over deeply dotted keys. While
a.b.c.d.e = 1is valid, it becomes hard to read. Break it into nested tables for anything beyond two levels. - Keep arrays of tables close together. Do not interleave unrelated configuration between elements of a
[[table]]array. Keep all array elements grouped for readability. - Validate in CI. Add
taplo checkto your CI pipeline alongside your linter and tests. Catch configuration errors before they reach production. - Provide a well-commented example file. Ship a
config.example.tomlwith your project that documents every available setting, its default, and its purpose.
Conclusion
TOML occupies a well-defined niche in the configuration format landscape. It is not trying to replace JSON for data interchange or YAML for complex infrastructure definitions. Instead, it does one thing well: provide a configuration file format that humans can read, write, and understand without surprises.
The format's adoption by Rust (Cargo), Python (pyproject.toml), Hugo, Netlify, and many other tools validates its design choices. A short specification, strict typing without implicit coercion, first-class dates and comments, and clean table syntax make TOML the right choice for configuration files that humans maintain.
Start with the basics: key/value pairs organized into tables. Learn arrays of tables when you need them. Use a validator like taplo to catch errors early. And remember that the best configuration format is the one your team can read and maintain without reaching for the documentation every time.