Pick up any Rust project and you'll find a Cargo.toml. Clone a GitHub repo and you'll find a .github/workflows/ folder full of YAML. Both formats are doing the same surface-level job — storing structured configuration that humans edit — but they make very different tradeoffs. If you've ever had YAML silently mangle a value on you, or wondered why Rust picked TOML instead of YAML for its package manifest, this is the article for you.

The Core Difference: Explicit vs Implicit

The fundamental split between TOML and YAML is about how much the parser is allowed to guess. TOML is explicit: every value has an unambiguous type. Strings are always quoted. Booleans are exactly true or false. Datetimes are first-class values with their own syntax. There is no implicit type coercion — the parser does not try to be clever.

YAML leans the other way. It tries to be convenient: you don't need quotes around most strings, and the parser infers types from the value's appearance. That inference is what bites people. The YAML 1.1 spec (still used by many tools) treats yes, no, on, off, true, and false all as booleans. It treats unquoted version strings like 1.0 as floats. And then there's the Norway Problem.

YAML's Famous Footguns

The Norway Problem became something of a meme in DevOps circles. In YAML 1.1, the country code NO parses as boolean false. So a config mapping ISO country codes to settings would silently convert Norway's entry into a boolean. The YAML 1.2 spec fixed this, but many widely-used parsers — including PyYAML's default mode until recently — still target YAML 1.1. Check which spec version your tooling actually implements before you trust it.

The Norway Problem in practice: In YAML 1.1, unquoted NO, Yes, on, and off all become booleans. The fix is simple — quote your strings — but the problem is that it's silent. Your config loads without error and you get a boolean where you expected a string.
yaml
# YAML 1.1 implicit type coercion — all of these silently become booleans:
countries:
  norway: NO        # → false  ← The Norway Problem
  sweden: SE        # → "SE"   (fine, not in the boolean list)
  enabled: yes      # → true
  disabled: no      # → false
  feature_flag: on  # → true
  another: off      # → false

# Version strings can become numbers:
python_version: 3.10   # → float 3.1 (trailing zero dropped)
api_version: 1.0       # → float 1.0

# Safe: quote anything that could be ambiguous
python_version: "3.10"
country: "NO"
enabled: "yes"
  • Octal literals. In YAML 1.1, 010 parses as 8 (octal), not 10. This matters for file permission values like 0755.
  • Tab vs space. YAML forbids tab characters for indentation. Paste in code from an editor configured to use tabs and you get a cryptic parse error — or worse, silent misalignment.
  • Implicit nulls. A key with no value becomes null. Easy to create by accident when editing by hand.
  • Indentation sensitivity. One extra space and a value silently shifts from one parent to another. No error, just wrong data.

TOML: What Explicit Types Actually Look Like

TOML's type system is spelled out in the TOML v1.0 spec. Strings must be quoted (single or double). Booleans are true or false and nothing else. Integers are integers. Floats are floats. And datetimes — something neither JSON nor YAML has as a native type — are first-class values in TOML.

toml
# TOML types are always unambiguous
name = "my-app"          # string — must be quoted
version = "1.0.0"        # string — quotes make it clear this is not a float
port = 8080              # integer
debug = false            # boolean — only true/false, nothing else
threshold = 0.95         # float

# Datetime is a first-class type — no string parsing needed
created_at = 2024-01-15T09:30:00Z
build_date = 2024-03-20

# Arrays
allowed_hosts = ["localhost", "staging.example.com", "api.example.com"]

# Inline tables
[database]
host = "postgres.internal"
port = 5432
name = "payments_prod"
pool_size = 20

The country code NO is just a string in TOML because it's not quoted, but TOML doesn't infer types from string-like values — unquoted values follow strict syntactic rules. To be a string, it needs quotes. TOML simply doesn't have the implicit coercion machinery that causes YAML's footguns.

Side by Side: A CI Pipeline Config

Here's a GitHub Actions workflow — this is YAML's home territory. The format fits naturally because GitHub Actions expects YAML, the indentation-based nesting mirrors the logical structure, and comments are essential for explaining non-obvious step configurations.

yaml
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build --if-present

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Now here's the equivalent project manifest in TOML — this is where TOML shines. Compare a real Cargo.toml structure with what the same config would look like in YAML:

toml
# Cargo.toml — Rust package manifest
[package]
name = "payments-service"
version = "2.4.1"
edition = "2021"
description = "Payment processing microservice"
license = "MIT"
authors = ["Alice Chen <[email protected]>"]

[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
tracing = "0.1"
anyhow = "1.0"

[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1

TOML's Weaknesses: Array of Tables

TOML isn't without rough edges. The syntax for arrays of tables — [[double brackets]] — is one of the more confusing things in the spec. It's how you express what JSON would write as an array of objects, and it reads strangely at first.

toml
# TOML array of tables — [[double brackets]] creates array entries
[[server]]
host = "web-01.example.com"
port = 443
region = "us-east-1"

[[server]]
host = "web-02.example.com"
port = 443
region = "us-west-2"

[[server]]
host = "web-03.example.com"
port = 443
region = "eu-west-1"

# The above is equivalent to this JSON:
# { "server": [
#   { "host": "web-01.example.com", "port": 443, "region": "us-east-1" },
#   { "host": "web-02.example.com", "port": 443, "region": "us-west-2" },
#   { "host": "web-03.example.com", "port": 443, "region": "eu-west-1" }
# ]}

In YAML the same structure reads more naturally — just a list with indented properties. For deeply nested data with repeated array entries, YAML's indentation-based nesting is genuinely less verbose than TOML's [[section]] headers. TOML also has no equivalent to YAML's anchor-and-alias system, so you can't define a shared block once and reference it elsewhere. If you find yourself duplicating the same set of values in multiple TOML tables, you're stuck copying them manually.

Where Each Format Wins in Practice

YAML wins by default in these ecosystems:

  • GitHub Actions. The entire workflow syntax is YAML. You don't get a choice here, and it's fine — the indentation maps well to the nested structure of jobs, steps, and conditions.
  • Kubernetes. Every manifest — Deployments, Services, ConfigMaps, Ingress rules — is YAML. The Kubernetes object model is deeply nested, and YAML handles that gracefully.
  • Docker Compose. Service definitions, networks, volumes — all YAML. Comments explaining why a port is exposed or why a specific healthcheck interval is used are part of the documentation.
  • Ansible. Playbooks, roles, variable files — YAML throughout. The comment support is actively used to explain non-obvious task parameters.

TOML wins when you control the format:

  • Rust projects. Cargo.toml is the gold standard. Dependency declarations, feature flags, build profiles — all in TOML. The explicit types mean version strings like "1.0.0" stay strings.
  • Python projects. pyproject.toml has become the standard for Python project metadata, build config, and tool settings (Black, isort, mypy, pytest all read from it).
  • Tool configuration where ambiguity causes bugs. If your config contains version strings, country codes, or other values that look like they might be booleans or numbers, TOML's explicit types eliminate a whole category of parsing surprises.
  • Flat configs. When your config is mostly key-value pairs at one or two levels of nesting, TOML is more readable than YAML and cleaner than JSON.

The Practical Decision Guide

Most of the time the choice is made for you by the ecosystem. GitHub Actions is YAML. Kubernetes is YAML. Rust is TOML. Python tooling is TOML. When you actually have a free choice, use this as your guide:

  • Use YAML when the tooling mandates it — CI/CD platforms, Kubernetes, Helm charts, Docker Compose, Ansible. Fighting the convention costs more than it saves.
  • Use YAML when you need anchors and aliases to keep a complex config DRY — there's no TOML equivalent.
  • Use TOML for project manifests and tooling config you control — package metadata, linter settings, build configuration.
  • Use TOML when your config contains values that YAML 1.1 might misinterpret — version strings, country codes, anything that resembles a boolean or number.
  • Quote your YAML strings whenever the value could be confused with a boolean, number, or null. Especially: version numbers, country codes, anything that starts with a digit, and values like yes, no, on, off.

Working with Both Formats

Need to validate or pretty-print a config file you're working on? The TOML Formatter handles TOML files, and the YAML Formatter covers YAML. If you need to migrate a config from one format to the other — say, converting a YAML-based tool config to TOML for a project that prefers it — the TOML to JSON and YAML to JSON converters both output JSON, which you can then convert to your target format. Sometimes going through JSON as an intermediate step is the most reliable path.

One thing worth knowing: both formats have their own JSON-compatible subsets. Valid JSON is valid YAML (YAML is a superset of JSON). TOML doesn't have that relationship with JSON, but the TOML spec is intentionally kept simple — it's a short read, which is more than you can say for the full YAML 1.2 specification.

Wrapping Up

TOML and YAML aren't really competing. They've settled into different niches based on their design priorities. YAML's implicit types and indentation-based structure make it the natural fit for large, nested configs maintained by teams — think Kubernetes manifests and GitHub Actions workflows. TOML's explicit types and flat section structure make it the natural fit for project manifests and tool configuration where a misread version string or country code would cause a real bug.

The one thing to carry with you: if you're writing YAML and you have any values that look like they could be booleans, numbers, or nulls — quote them. It's the single habit that prevents most YAML-related config bugs. And if you're starting a new project and get to choose your config format freely, TOML is worth a look. The TOML GitHub repository has good examples, and once you've written a Cargo.toml or pyproject.toml, the format's appeal becomes pretty clear.