TOML exists because YAML is too complex for configuration files and JSON doesn't support comments. Tom Preston-Werner created TOML in 2013 with a single goal: a config format that maps unambiguously to a hash table (dictionary/object) and is easy for humans to read and write. No indentation footguns. No implicit type coercion. No 86-page specification.

TOML looks like an INI file that grew up and got a type system. If you've ever written a config file with [section] headers and key = value pairs, you already know 80% of TOML. The remaining 20% — arrays of tables, inline tables, and multiline strings — handles the cases where INI falls short.

TOML v1.0.0 was finalized in January 2021, giving it a stable specification after eight years of development. It's now the standard for Rust's Cargo, Python's pyproject.toml, Hugo, Netlify, and an expanding set of tools that want config simplicity without YAML's baggage.

TOML Syntax: Clear, Typed, and Unsurprising

TOML's syntax is built on key-value pairs organized into tables (sections):

# This is a comment
title = "My Application"

[server]
host = "localhost"
port = 8080
debug = false

[database]
connection_string = "postgres://localhost/mydb"
max_connections = 25
connection_timeout = 30.0

[database.pool]
min_size = 5
max_size = 25

Key features of TOML syntax:

  • Comments use # and run to end of line. No multiline comments.
  • Keys are bare (unquoted) if they contain only letters, digits, dashes, and underscores. Otherwise, quote them: "key with spaces" = "value".
  • Tables (sections) use [table_name] headers, like INI files. Dotted keys ([database.pool]) create nested tables.
  • No indentation requirements. Indentation is purely cosmetic. You can indent for readability or not — the parser doesn't care.

TOML's Type System

TOML has the strongest type system of any common config format:

TypeExampleNotes
Stringname = "hello"Always quoted. Basic (double) or literal (single) quotes.
Integerport = 808064-bit signed. Supports _ separators: 1_000_000
Floatpi = 3.1415964-bit IEEE 754. Supports inf, nan
Booleandebug = trueOnly true and false. Not "yes", not "on".
Offset Date-Timecreated = 2026-03-19T14:30:00ZISO 8601 with timezone. Native type, not a string.
Local Date-Timealarm = 2026-03-19T07:00:00No timezone offset.
Local Datebirthday = 1990-05-15Date only, no time.
Local Timestart = 09:00:00Time only, no date.
Arrayports = [8080, 8443]Must be same type. Multiline allowed.
Inline Tablepoint = {x = 1, y = 2}Single-line table. Cannot be multiline.

TOML is the only common data format with native date/time types. JSON uses strings. YAML auto-parses ISO dates but that behavior is inconsistent across parsers. CSV has no types at all. In TOML, date = 2026-03-19 is unambiguously a date — not a string that happens to look like a date.

Tables, Nested Tables, and Arrays of Tables

TOML's table syntax is where it goes beyond INI files:

# Simple table
[owner]
name = "John"
email = "john@example.com"

# Nested table (equivalent to owner.address)
[owner.address]
street = "123 Main St"
city = "Springfield"

# Array of tables (array of objects)
[[products]]
name = "Widget"
price = 9.99

[[products]]
name = "Gadget"
price = 19.99

The [[double_bracket]] syntax for arrays of tables is TOML's most powerful and least intuitive feature. Each [[products]] block creates a new element in an array. This maps to JSON's "products": [{"name": "Widget", "price": 9.99}, {"name": "Gadget", "price": 19.99}].

This syntax works well for lists of structured items (packages, servers, routes) but becomes verbose for deeply nested arrays. A three-level nested array of tables requires [[level1.level2.level3]] headers for each element at the deepest level — manageable for config files, unwieldy for complex data.

Where TOML Is the Standard

TOML has been adopted as the default config format by several major ecosystems:

EcosystemFilePurpose
Rust (Cargo)Cargo.tomlPackage manifest, dependencies, build config
Python (PEP 518/621)pyproject.tomlProject metadata, build system, tool config
Go modulesgo.sum uses TOML-like formatDependency checksums
Hugoconfig.tomlSite configuration
Netlifynetlify.tomlBuild and deploy configuration
pippip.conf supports TOMLpip configuration
Black (Python)[tool.black] in pyproject.tomlFormatter configuration
Ruff (Python)[tool.ruff] in pyproject.tomlLinter configuration

The pattern: newer tools and ecosystems favor TOML. Older ecosystems (Java, .NET, DevOps) remain on XML or YAML. Rust's early adoption of TOML gave it legitimacy; Python's PEP 518 (2017) and PEP 621 (2020) made it mainstream.

TOML vs. YAML: Different Tools for Different Jobs

TOML and YAML serve the same purpose (config files) but make different tradeoffs:

FeatureTOMLYAML
IndentationIrrelevant (cosmetic only)Structural (bugs if wrong)
Type coercionNone (all types explicit)Aggressive (Norway problem)
Boolean valuestrue/false only12+ variants in YAML 1.1
Date typesNative (4 date/time types)Auto-detected from strings
Deep nestingVerbose (long table headers)Natural (indentation)
Comments# (line only)# (line only)
Spec complexity~30 pages~86 pages
Learning curveMinutesHours (including footguns)

TOML wins for flat-to-moderately-nested configuration. YAML wins for deeply nested structures (like Kubernetes manifests with 5+ nesting levels). For a simple app config with database settings, feature flags, and API keys, TOML is the safer choice. For orchestrating 50 containers with networking, volumes, and resource limits, YAML's deep nesting handles it better.

You can convert TOML to YAML or YAML to TOML when switching between ecosystems. The conversion is lossless in both directions for data that fits TOML's structure.

Where TOML Falls Short

TOML's simplicity is also its limitation:

  • Deep nesting is verbose. A 4-level nested structure requires table headers like [level1.level2.level3.level4] for each leaf, making the file harder to read than equivalent YAML. TOML is designed for 2-3 levels of nesting maximum.
  • No anchors or references. YAML's anchors let you define a value once and reference it multiple times. TOML has no equivalent — you must duplicate values or handle deduplication in application code.
  • Heterogeneous arrays are forbidden. mixed = [1, "two", true] is invalid TOML. Every element in an array must be the same type. This prevents TOML from representing some valid JSON structures.
  • No multi-line inline tables. Inline tables ({key = "value"}) must fit on one line. For complex nested objects, you must use full [table] sections.
  • Smaller ecosystem. YAML has decades of tooling, editor support, and library maturity. TOML libraries exist for every major language but are generally less battle-tested.

Converting TOML to Other Formats

TOML sits between INI (simpler) and YAML (more complex) in the config format spectrum:

ConversionWhat HappensData Loss?
TOML to JSONDirect mapping. Date types become ISO 8601 strings.Date type information (becomes string)
TOML to YAMLDirect mapping. Dates preserved if parser supports them.No
TOML to CSVOnly works for flat tables or arrays of tables.Yes — nesting, types
TOML to XMLTables become elements. Arrays become repeated elements.No (structurally lossless)
TOML to INIWorks for single-level tables. Nested tables may be flattened.Yes — types, nesting
TOML to ENVOnly top-level key-value pairs. Everything becomes a string.Yes — types, nesting, arrays

TOML is the right tool for a specific job: configuration files that humans read and edit, with moderate structure and explicit types. It doesn't try to be a general data format like JSON or a do-everything serialization language like YAML. That narrow focus is its strength — fewer features means fewer surprises.

If your config file has sections, key-value pairs, maybe some lists and a few nested sections, TOML is the best choice. If you need deeply nested structures, heterogeneous arrays, or anchors, use YAML. If you need to exchange data between systems, use JSON. TOML's lane is clear, and it stays in it.