Skip to content
Back to Blog
informational

What Is TOML? The Config Language That Beat YAML

2026-05-17 9 min read

What TOML Actually Is

TOML stands for Tom's Obvious, Minimal Language. Its creator, GitHub co-founder Tom Preston-Werner, was fed up with the existing config formats back in 2013. The language matured for nearly eight years before the first stable release, TOML v1.0.0, landed in January 2021. That long refinement period tells you how seriously its design was considered. At its heart, TOML is a configuration file format. It is not a general-purpose data serialization format like JSON, nor is it a markup language. The entire goal is to be trivial for a human to read and write, while mapping unambiguously to a hash table—or what you might call a dictionary, map, or object in your language of choice. A minimal TOML file is dead simple: ``` title = "My Application" version = "2.4.1" debug = false [database] host = "localhost" port = 5432 max_connections = 100 ``` Every value has an explicit type. `"localhost"` is a string. `5432` is an integer. `false` is a boolean—not the string `"false"`, not `0`, and not `null`. This strictness is the whole point, and it's a primary reason developers choose TOML. You'll never waste time wondering if your port number will be parsed as a string or an integer because of some quirk in a specific YAML library.

Why YAML Became a Problem Worth Solving

YAML gets a lot of credit for being human-friendly. For tiny files, that's true. But its friendliness disappears quickly as your configuration grows, and its design choices start to bite. The most infamous example is the Norway Problem. In YAML 1.1—still the default for many parsers—the two-letter country code `NO` is parsed as the boolean value `false`. A configuration like `country: NO` would silently corrupt your data. This applies to `yes`, `no`, `on`, `off`, `true`, and `false` in various capitalizations. YAML 1.2 fixed this, but you can't always control which parser version your tools are using. Then there's the significant whitespace. YAML uses indentation to define structure, so a single misplaced space can silently change your file's entire meaning or just break it completely. Anyone who has burned an hour debugging a CI/CD pipeline only to discover a two-space versus four-space inconsistency knows this pain intimately. YAML also offers too many ways to write the same thing. Scalars can be plain, single-quoted, double-quoted, or block style. Sequences can be flow or block style. This flexibility sounds nice, but it means two developers will write the same logical configuration in completely different ways, making diffs harder to read and code reviews less effective. This doesn't make YAML useless. It's the standard for Kubernetes manifests and GitHub Actions workflows for a reason. But for application configuration, where correctness and predictability matter more than anything, these quirks are a serious liability.

How TOML Solves the Readability Problem

TOML's philosophy is simple: there should be one, and only one, obvious way to write something. It sounds restrictive, but the result is configuration files that look and feel the same, no matter who wrote them or what project they belong to. It offers six scalar types: string, integer, float, boolean, offset date-time, and local date. TOML's first-class support for dates and times in RFC 3339 format is a killer feature. You can write `created_at = 2024-03-15T09:30:00Z` and trust it will become a proper datetime object, not a string you have to parse yourself. While YAML can represent dates, the behavior is inconsistent across parsers. Structure is defined with tables (sections) and arrays of tables. A table gets a header in square brackets: `[server]`. An array of tables uses double square brackets: `[[products]]`. The syntax is unambiguous and easy to spot. Here’s a real-world example from a Rust `Cargo.toml` file, showing how dependencies are defined: ``` [dependencies] serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } reqwest = "0.12" ``` The inline table syntax—the part in curly braces—is great for simple, compact definitions. For more complex nested data, you use full table headers. The language gives you clear rules for when to use each style. Even comments are designed for familiarity. They use the `#` character, just like Python and shell scripts. Most developers already know the syntax without reading a single line of the spec.

Where TOML Has Won: Real Adoption Numbers

The Rust ecosystem is TOML's biggest success story. Cargo, Rust's package manager, mandates TOML for its manifests. With over 150,000 packages on crates.io as of early 2025, every single one has a `Cargo.toml` file. That's a massive, real-world stress test that few formats have ever passed. Python's adoption via PEP 518 (2016) and PEP 621 (2021) cemented `pyproject.toml` as the one true place for project metadata and build configuration. Tools like Poetry, Hatch, Flit, and PDM all use it. Your linter settings go in `[tool.ruff]`, your test settings in `[tool.pytest.ini_options]`. You get one file and one format to rule them all. Hugo, the popular static site generator, made TOML its default configuration format, moving away from YAML and JSON. The team specifically cited TOML's explicitness and lack of surprising type coercions as the motivation. When a language adds a parser to its standard library, you know the format has arrived. Python did just that by adding `tomllib` in version 3.11 (released October 2022), letting you parse TOML files on any modern Python install with zero external dependencies. It's not just a Python and Rust thing, either. Go, .NET, and JavaScript all have mature, well-maintained TOML libraries. The `@iarna/toml` package on npm, for instance, sees millions of weekly downloads. TOML is officially mainstream.

TOML's Genuine Limitations

No tool is perfect, and TOML is no exception. It's important to be honest about its limitations so you know when to reach for something else. Deeply nested data is TOML's Achilles' heel. If you need more than two or three levels of nesting, the syntax gets painful. You'll find yourself writing long, dotted keys like `[servers.production.database.replica]`. It's valid, but it's not readable. JSON and even YAML are simply better at this because they were built for general data representation. Large arrays of complex objects are another weak spot. The `[[products]]` syntax for an array of tables means you have to repeat that header for every single item. A list of 50 products results in 50 separate `[[products]]` headers. At that point, you're not writing a config file; you're using the wrong tool. You should be using JSON or a database. TOML lacks support for anchors and aliases, a feature YAML provides with `&anchor` and `*alias`. This means you can't define a block of settings once and reuse it across your file. If you need to repeat configuration, you have two choices: duplicate it directly, or build merging logic into your application code. There's no built-in way to keep it DRY. Finally, you can't stream a TOML file. Unlike JSON Lines, a TOML document must be read and parsed in its entirety before you can access any values. This can matter for enormous configuration files, though a config file that large is often a symptom of a deeper design issue. These limitations don't make TOML a bad choice for its intended purpose: application configuration of moderate complexity. They just define its boundaries.

Converting Between TOML and Other Formats

If you're migrating a project from YAML or just need to convert config data between formats for a tool, you have a few options. To convert programmatically in Python, you can combine the `tomllib` (read) and `tomli-w` (write) libraries with a YAML parser like `PyYAML`. Reading a YAML file into a Python dictionary with `yaml.safe_load()` and then writing it out with `tomli_w.dumps()` works, but it's best for flat or moderately nested files. Deeply nested structures, YAML anchors, or multi-document files will require manual cleanup. For JSON-to-TOML conversion, the mapping is much cleaner since both formats have explicit types. A JSON integer becomes a TOML integer. A JSON boolean becomes a TOML boolean. The main structural shift is turning JSON arrays of objects into TOML arrays of tables (`[[table]]`), which a good converter can handle. Online tools like CocoConvert can handle the conversion for you. Upload a JSON or YAML file, choose TOML as the output format, and the converter does the work. This is a great option for straightforward config files. For files with YAML-specific features like anchors or deeply nested structures, you'll still need to review the output. CocoConvert will flag conversion warnings when it finds something that doesn't map cleanly to TOML, but it can't refactor your data structure for you—that's a decision you have to make. For bulk migrations, like converting an entire repository of 40 YAML files, uploading them one-by-one is a non-starter. The CocoConvert API is built for this, letting you batch the requests and automate the entire process.

Should You Switch Your Project to TOML?

So, should you switch? The answer depends on your project and what its toolchain expects. For a new Python project in 2025, the answer is an emphatic yes. Use `pyproject.toml`. The ecosystem has standardized on it, and fighting this just creates needless friction. Don't be the person creating a separate `ruff.toml` when the standard is to use the `[tool.ruff]` section in the project's main config file. If you're writing a Rust project, this isn't even a question. Cargo uses TOML, and that's a feature, not a bug. The format's consistency is a big part of why Rust's tooling feels so coherent. For an existing project with working YAML configuration, I'd say migrate only if that YAML is a source of pain. If your `config.yaml` is stable and the team understands it, the cost of switching—updating docs, scripts, and people's habits—probably isn't worth the 'purity' of using TOML. In the world of Kubernetes and CI/CD (GitHub Actions, GitLab CI, etc.), YAML is king. You don't have a choice, and TOML isn't trying to dethrone it in that arena. The real litmus test is this: are you writing comments in your YAML file to explain what data type a value *should* be? Are you chasing bugs caused by a number being read as a string? That's your signal. You've outgrown YAML's ambiguity. TOML was designed to solve exactly this class of problem by making types explicit from the very start.