If you've opened a Rust project, you've met TOML. If you've touched a Python package that uses pyproject.toml, you've used it too. TOML — Tom's Obvious Minimal Language — is the config file format that keeps quietly winning over developers who are tired of YAML's whitespace footguns and JSON's refusal to allow comments. Created by Tom Preston-Werner, co-founder of GitHub, TOML was designed around one idea: a config format should be so obvious that you can read it without a spec. Let's see if it lives up to that promise.

What Exactly Is TOML?

TOML is a configuration file format with three explicit design goals, spelled out right at the top of the official spec: it should be obvious to read, minimal in complexity, and map unambiguously to a hash table (a dictionary/map in most languages). That third goal is the key one — every valid TOML file has exactly one correct parse result. No surprising type coercion, no YAML-style boolean landmines, no ambiguity about whether a value is a string or a number.

The format borrows the section-header style from old INI files but adds proper types, arrays, and nested tables on top. The result is something that feels familiar to anyone who's edited a config file before, but with enough structure that a parser can give you a real typed data model out of it. Version 1.0.0 of the spec was released in January 2021 after years of refinement — you can browse the full TOML spec on GitHub if you want to dig into the edge cases.

The Basics: Key-Value Pairs and Comments

A TOML file is made up of key-value pairs. Keys and values are separated by =, and comments start with #. Simple.

toml
# Cargo.toml — Rust package manifest
[package]
name = "image-resizer"
version = "0.4.2"
edition = "2021"
authors = ["Ada Lovelace <[email protected]>"]
description = "Fast image resizing with Lanczos3 resampling"
license = "MIT"
repository = "https://github.com/example/image-resizer"

# Integers, floats, booleans — all native types
max_threads = 8
quality_default = 0.85
verbose_logging = false

No quotes required on most keys. Values are typed — 8 is an integer, 0.85 is a float, false is a boolean. No guessing, no implicit coercion based on how the value looks. This is the everyday TOML you'll write 90% of the time.

String Types: Basic, Literal, and Multiline

TOML has four string types. This covers every real-world case cleanly:

toml
# Basic strings — double quotes, support escape sequences
greeting = "Hello, \nworld!"
path = "C:\\Users\\ada\\Documents"

# Literal strings — single quotes, no escape processing at all
regex_pattern = '\d{4}-\d{2}-\d{2}'
windows_path = 'C:\Users\ada'

# Multiline basic string — triple double quotes
sql_query = """
  SELECT user_id, email, created_at
  FROM users
  WHERE active = true
    AND created_at > '2024-01-01'
  ORDER BY created_at DESC
"""

# Multiline literal string — triple single quotes, no escapes
shell_script = '''
#!/bin/bash
echo "Deploying $APP_NAME to $ENV"
kubectl apply -f k8s/
'''

The literal string type ('single quotes') is the one people forget exists, and it's genuinely useful — regex patterns and Windows paths are much cleaner without escape doubling. Pick whichever quote style means you write fewer backslashes.

Numbers, Booleans, and Datetimes

TOML's native types cover everything you'd actually put in a config file. Notably, it has first-class datetime support — something YAML technically has but handles inconsistently across parsers.

toml
# Integers — underscores allowed as separators (like numeric literals in code)
max_connections = 1_000_000
port = 5432
hex_color = 0xFF6B6B      # hex prefix supported
octal_permissions = 0o755  # octal prefix supported

# Floats
pi = 3.14159265
compression_ratio = 1.5e-3
infinity_val = inf         # special values: inf, -inf, nan

# Booleans — lowercase only (not True, TRUE, yes, on)
ssl_enabled = true
dry_run = false

# Datetimes — RFC 3339 format
created_at = 2024-03-15T09:30:00Z
updated_at = 2024-03-15T14:22:10+05:30
log_date = 2024-03-15           # local date (no time)
backup_time = 03:00:00          # local time (no date)
TOML booleans are strictly lowercase. true and false only — not True, TRUE, yes, on, or any of the other variants YAML 1.1 accepts. This is intentional. If you need a string "true", quote it.

Tables: The INI-Style Section Headers

Tables in TOML are defined with [header] syntax. Everything below a header belongs to that table until the next header appears. This is the feature that makes TOML look familiar — it's essentially INI files but with types.

toml
[database]
host = "db.internal"
port = 5432
name = "app_production"
pool_size = 20

[database.credentials]
username = "app_user"
# Don't put real passwords here — use env vars or a secrets manager
password_env = "DB_PASSWORD"

[server]
host = "0.0.0.0"
port = 8080
workers = 4

[server.tls]
enabled = true
cert_file = "/etc/ssl/certs/app.crt"
key_file = "/etc/ssl/private/app.key"

Dotted headers like [database.credentials] create nested tables. The parsed result is exactly what you'd expect: a database object with a nested credentials object. You can also write inline tables for simple cases — more on that below.

Arrays and Array of Tables

Arrays in TOML use square brackets and can span multiple lines. The really distinctive TOML feature is Array of Tables — defined with double brackets [[header]]. This is TOML's answer to "how do I express a list of objects?" without it looking like JSON.

toml
# Regular arrays — can be split across lines, trailing comma is fine
allowed_origins = [
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]

supported_formats = ["jpeg", "png", "webp", "avif"]
retry_delays_ms = [100, 250, 500, 1000, 2000]

# Array of Tables — [[double brackets]]
# Each [[servers]] header appends a new object to the servers array
[[servers]]
name = "web-01"
ip = "10.0.1.10"
role = "primary"
tags = ["web", "prod"]

[[servers]]
name = "web-02"
ip = "10.0.1.11"
role = "replica"
tags = ["web", "prod"]

[[servers]]
name = "db-01"
ip = "10.0.2.10"
role = "primary"
tags = ["database", "prod"]

That [[servers]] syntax parses into an array of three objects — equivalent to "servers": [{...}, {...}, {...}] in JSON. It's verbose compared to JSON arrays of objects, but the advantage is readability when each item has many fields. You can see this pattern heavily in Cargo.toml manifests for defining multiple binary targets, examples, and bench entries.

Inline Tables: Compact One-Liners

When a table has just a couple of fields and you don't want a whole section header for it, inline tables let you write it on a single line:

toml
[build]
# Inline table — must stay on one line
target = { arch = "x86_64", os = "linux", libc = "musl" }

# Equivalent to writing:
# [build.target]
# arch = "x86_64"
# os = "linux"
# libc = "musl"

[feature_flags]
auth   = { enabled = true, rollout_pct = 100 }
search = { enabled = true, rollout_pct = 50 }
beta   = { enabled = false, rollout_pct = 0 }

Inline tables must stay on a single line and cannot be extended later with a [header]. Use them for small, cohesive value groups — they're great for things like coordinate pairs, build targets, or simple flag configs. Don't use them when you have more than three or four fields; at that point a regular table reads better.

TOML in the Real World

TOML has carved out a strong niche in the Rust and Python ecosystems, and is gaining traction elsewhere. Here's where you'll encounter it day-to-day:

  • Rust — Cargo.toml: Every Rust project has one. Defines package metadata, dependencies, features, and build targets. The Cargo manifest reference is the most detailed real-world TOML usage guide you'll find.
  • Python — pyproject.toml: PEP 518 and PEP 621 standardised TOML as the Python project metadata format. Poetry, Hatch, PDM, and setuptools all read from it.
  • Deno: The Deno config file supports TOML in addition to JSON.
  • Hugo: The Hugo static site generator accepts TOML as config and front-matter format — you'll see it between +++ delimiters at the top of Markdown files.
  • uv: The fast Python package manager from Astral uses pyproject.toml for all configuration, making TOML the de-facto standard for new Python tooling.

If you need to convert an existing config between formats, the TOML to JSON and JSON to TOML converters handle the structural mapping for you. Or use the TOML Formatter to clean up inconsistent spacing in an existing file, and the TOML Validator to catch syntax errors before they surface in production.

Parsing TOML in Python

Since Python 3.11, the standard library includes tomllib — no external dependency needed. For Python 3.9 and 3.10, the tomli backport has an identical API, so you can switch between them with a single import alias.

python
import sys

if sys.version_info >= (3, 11):
    import tomllib
else:
    import tomli as tomllib  # pip install tomli

# tomllib only reads binary mode — open with "rb"
with open("pyproject.toml", "rb") as f:
    config = tomllib.load(f)

# Types match TOML exactly: str, int, float, bool, datetime, list, dict
project_name = config["project"]["name"]               # str
python_requires = config["project"]["requires-python"] # str
dependencies = config["project"]["dependencies"]        # list[str]

print(f"Project: {project_name}")
print(f"Requires Python: {python_requires}")
print(f"Dependencies ({len(dependencies)}):")
for dep in dependencies:
    print(f"  {dep}")

# Parse from a string with tomllib.loads()
raw = """
[server]
host = "localhost"
port = 8080
debug = true
"""
server_config = tomllib.loads(raw)
print(server_config["server"]["port"])   # 8080 (int, not "8080")

Note that tomllib.load() requires binary mode ("rb"). This is intentional — TOML requires UTF-8 encoding, and opening in binary mode lets the parser handle the encoding check itself. It's a small gotcha that catches people the first time.

Parsing TOML in Rust

In Rust, the toml crate is the standard choice. It integrates tightly with serde, so you can deserialise directly into your own structs with minimal boilerplate:

rust
use serde::Deserialize;
use std::fs;

#[derive(Debug, Deserialize)]
struct AppConfig {
    server: ServerConfig,
    database: DatabaseConfig,
    feature_flags: FeatureFlags,
}

#[derive(Debug, Deserialize)]
struct ServerConfig {
    host: String,
    port: u16,
    workers: usize,
}

#[derive(Debug, Deserialize)]
struct DatabaseConfig {
    host: String,
    port: u16,
    name: String,
    pool_size: u32,
}

#[derive(Debug, Deserialize)]
struct FeatureFlags {
    enable_beta: bool,
    max_upload_mb: u32,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let raw = fs::read_to_string("config.toml")?;
    let config: AppConfig = toml::from_str(&raw)?;

    println!("Server: {}:{}", config.server.host, config.server.port);
    println!("DB pool size: {}", config.database.pool_size);
    println!("Beta enabled: {}", config.feature_flags.enable_beta);

    Ok(())
}

Add toml = "0.8" and serde = { version = "1", features = ["derive"] } to your [dependencies] in Cargo.toml and you're set. The serde derive macros handle all the field mapping. If your struct field names use snake_case but the TOML keys use kebab-case, add #[serde(rename_all = "kebab-case")] at the struct level and everything maps automatically.

TOML vs YAML vs JSON — When to Pick Which

This question comes up on every new project. Here's the honest breakdown:

  • TOML: Best for config files that humans write and maintain, where typed values matter and nesting is shallow-to-moderate. Sweet spot: app configs, build manifests, tool settings. Falls apart for deep nesting — 4+ levels becomes awkward with repeated section headers.
  • YAML: Best when you're writing structured data with lots of list-of-objects (Kubernetes manifests, GitHub Actions workflows). Multi-line string support is genuinely better than TOML's. The downside: whitespace-sensitivity and YAML 1.1 type coercion create real bugs in the wild.
  • JSON: Best for machine-to-machine data exchange, APIs, and when you need the broadest possible toolchain support. Not great for human-maintained config — no comments, and string escaping is tedious.
Quick rule of thumb: If a developer is editing the file by hand in a text editor, TOML is usually the most pleasant experience. If it's generated by a tool or consumed by a dozen different systems in different languages, JSON's universality wins. YAML lives in the middle — great for DevOps config that humans write but tooling processes heavily.

A Real pyproject.toml

To put everything together, here's a realistic pyproject.toml for a Python library — the kind you'd find on a modern open-source project. Notice how the format carries a lot of structured information while still being easy to scan:

toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "httpx-cache"
version = "1.2.0"
description = "Transparent HTTP caching layer for httpx"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.9"
authors = [
  { name = "Ada Lovelace", email = "[email protected]" },
]
keywords = ["http", "cache", "httpx", "async"]
classifiers = [
  "Development Status :: 4 - Beta",
  "Intended Audience :: Developers",
  "License :: OSI Approved :: MIT License",
  "Programming Language :: Python :: 3.9",
  "Programming Language :: Python :: 3.10",
  "Programming Language :: Python :: 3.11",
  "Programming Language :: Python :: 3.12",
]
dependencies = [
  "httpx>=0.25.0",
  "anyio>=4.0.0",
]

[project.optional-dependencies]
redis = ["redis>=5.0.0"]
dev = [
  "pytest>=7.4.0",
  "pytest-asyncio>=0.23.0",
  "coverage[toml]>=7.3.0",
  "ruff>=0.1.0",
  "mypy>=1.7.0",
]

[project.urls]
Homepage = "https://github.com/example/httpx-cache"
Changelog = "https://github.com/example/httpx-cache/blob/main/CHANGELOG.md"

[tool.ruff]
line-length = 100
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "UP"]

[tool.mypy]
strict = true
python_version = "3.9"

[tool.coverage.run]
source = ["httpx_cache"]
branch = true

[tool.coverage.report]
fail_under = 90

This is real TOML doing real work. Each [tool.x] section is a separate namespace for a different tool — ruff, mypy, coverage — all living in one file without stepping on each other. No deep nesting required, everything is readable at a glance.

Wrapping Up

TOML delivers on its promise: readable, unambiguous, and cleanly typed. If you're starting a new project in Rust, Python, or Go, TOML is worth defaulting to for config files — especially files that will be committed to source control and edited by multiple people. The lack of whitespace-sensitivity alone makes it a relief compared to YAML. For working with TOML files directly in your browser, the TOML Formatter and TOML Validator handle the most common tasks. And if you're migrating an existing project from JSON or need to bridge TOML into a JSON-based pipeline, the TOML to JSON and JSON to TOML converters have you covered.