What Is YAML?
YAML — YAML Ain't Markup Language (a recursive acronym) — is a human-readable data serialization format designed to be easy to write by hand and easy to read without special tooling. First released in 2001, YAML is a superset of JSON (every valid JSON document is valid YAML 1.2), but its primary appeal is its cleaner, indent-based syntax for configuration files and structured data that humans need to read and write frequently.
YAML has become the dominant configuration format for developer tooling: Docker Compose, Kubernetes manifests, GitHub Actions workflows, Ansible playbooks, Travis CI, GitLab CI, Helm charts, and countless application configuration files all use YAML.
YAML Syntax Fundamentals
Scalars (Strings, Numbers, Booleans, Null)
# Strings — can be quoted or unquoted
name: Alice
message: "Hello, World!"
multiword: this is a string without quotes
# Numbers
port: 8080
version: 3.14
hex_value: 0xFF # interpreted as 255
# Booleans — YAML 1.1 vs 1.2 difference (see below)
enabled: true
disabled: false
# Null
database: null
optional_field: ~ # ~ is also null
Sequences (Lists)
# Block style (recommended for readability)
fruits:
- apple
- banana
- cherry
# Flow style (compact, like JSON)
colors: [red, green, blue]
# Nested sequences
matrix:
- [1, 2, 3]
- [4, 5, 6]
- [7, 8, 9]
Mappings (Dictionaries)
# Block style
server:
host: localhost
port: 5432
database: mydb
credentials:
username: admin
password: secret
# Flow style (compact, like JSON objects)
point: {x: 10, y: 20}
# Mixed types in a mapping
config:
name: MyApp
version: 2.1
enabled: true
features:
- feature_a
- feature_b
metadata: null
Multi-Line Strings
YAML provides two styles for multi-line strings:
# Literal block scalar (|) — preserves newlines
description: |
This is the first line.
This is the second line.
Trailing newline is preserved.
# Folded block scalar (>) — folds newlines to spaces
summary: >
This is a long paragraph that wraps
across multiple lines but is folded
into a single line by YAML parsers.
# Literal with chomping
no_trailing_newline: |-
Exact content
no trailing newline (- strips it)
extra_newlines: |+
Content
(+ keeps extra trailing newlines)
Anchors (&) and Aliases (*)
Anchors allow you to define a value once and reference it multiple times, a critical feature for avoiding repetition in configuration files:
# Define a reusable block with an anchor
defaults: &defaults
restart: always
logging:
driver: json-file
options:
max-size: "10m"
# Merge into another mapping
web:
<<: *defaults # merge defaults here
image: nginx:latest
ports:
- "80:80"
api:
<<: *defaults # same defaults
image: myapp:latest
ports:
- "3000:3000"
# Simple scalar anchor
database_host: &db_host postgres.internal
connection_string: "postgresql://*db_host/mydb"
# String anchor reference (works in anchored value, not interpolation)
primary_db: &primary_host db1.example.com
replica_db: *primary_host
The <<: merge key flattens the referenced mapping into the current one, with the current mapping's keys taking precedence over merged keys.
The YAML 1.1 vs 1.2 Difference
The Norway problem (infamous in YAML circles) stems from YAML 1.1 treating certain strings as booleans:
# YAML 1.1 (PyYAML 5.x and earlier, many legacy parsers):
# These are all interpreted as boolean true/false:
enabled: yes # True
disabled: no # False ← "NO" → false, so Norway's ISO code "NO" becomes false!
flag: on
off_switch: off
# YAML 1.2 (ruamel.yaml, PyYAML 6+):
# Only 'true'/'false' are booleans. 'yes'/'no'/'on'/'off' are strings.
country_code: NO # String "NO" — correct
Always use true/false for booleans, and quote strings that could be misinterpreted:
# Safe — unambiguous in all YAML versions
enabled: true
country: "NO"
status: "on"
Working with YAML in Python
PyYAML: The Standard Library
pip install pyyaml
import yaml
# ── Reading YAML ──────────────────────────────────────────────────────────
# From a string
yaml_string = """
server:
host: localhost
port: 8080
features:
- auth
- logging
"""
config = yaml.safe_load(yaml_string)
print(config['server']['host']) # localhost
print(config['features']) # ['auth', 'logging']
# From a file
with open('config.yaml', 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
# From a file with multiple documents (separated by ---)
with open('multi.yaml', 'r') as f:
documents = list(yaml.safe_load_all(f))
# ── Writing YAML ──────────────────────────────────────────────────────────
data = {
'name': 'MyApp',
'version': '2.1.0',
'server': {'host': 'localhost', 'port': 8080},
'features': ['auth', 'cache', 'logging'],
}
# To string
yaml_str = yaml.dump(data, default_flow_style=False, allow_unicode=True)
print(yaml_str)
# To file
with open('output.yaml', 'w', encoding='utf-8') as f:
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
Important: Always use yaml.safe_load() instead of yaml.load(). The unsafe load() can execute arbitrary Python code when parsing untrusted YAML — a critical security vulnerability.
ruamel.yaml: Roundtrip-Safe YAML (Preserves Comments)
PyYAML does not preserve comments when reading and re-writing YAML. ruamel.yaml does:
pip install ruamel.yaml
from ruamel.yaml import YAML
def update_yaml_preserve_comments(filepath: str, key_path: str, new_value) -> None:
"""
Update a value in a YAML file while preserving comments and formatting.
Args:
key_path: Dot-separated path, e.g. 'server.port'
"""
yaml = YAML()
yaml.preserve_quotes = True
with open(filepath, 'r') as f:
data = yaml.load(f)
# Navigate to key
keys = key_path.split('.')
obj = data
for key in keys[:-1]:
obj = obj[key]
obj[keys[-1]] = new_value
with open(filepath, 'w') as f:
yaml.dump(data, f)
# Example: update server.port without destroying comments
update_yaml_preserve_comments('config.yaml', 'server.port', 9090)
YAML Validation with Pydantic
import yaml
from pydantic import BaseModel, validator
from typing import Optional, List
class ServerConfig(BaseModel):
host: str
port: int
database: str
class AppConfig(BaseModel):
name: str
version: str
server: ServerConfig
features: List[str] = []
debug: bool = False
def load_and_validate_config(yaml_path: str) -> AppConfig:
"""Load YAML and validate structure with Pydantic."""
with open(yaml_path, 'r') as f:
raw = yaml.safe_load(f)
return AppConfig(**raw) # Raises ValidationError if invalid
try:
config = load_and_validate_config('app_config.yaml')
print(f"Config loaded: {config.name} v{config.version}")
print(f"Server: {config.server.host}:{config.server.port}")
except Exception as e:
print(f"Invalid config: {e}")
YAML in Real-World Configurations
Docker Compose (docker-compose.yml)
version: "3.9"
services:
web:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
app:
build: .
environment:
DATABASE_URL: postgresql://db:5432/myapp
DEBUG: "false"
restart: unless-stopped
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: secret
volumes:
postgres_data:
GitHub Actions Workflow (.github/workflows/ci.yml)
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest tests/ -v --cov=src
Common YAML Pitfalls
Implicit type coercion — YAML tries to infer types from unquoted scalars. version: 1.0 is a float; version: "1.0" is a string. Quote values that should remain strings.
Indentation with tabs — YAML prohibits tabs for indentation. Use spaces only. Most editors should be configured with "YAML: no tabs" or equivalent.
Special characters in strings — Characters :, {, }, [, ], ,, #, &, *, ?, |, -, <, >, =, !, %, @, ` can cause parsing issues when unquoted.
Duplicate keys — YAML technically allows duplicate keys in a mapping, but behavior is undefined (last key wins in most parsers). Validators like yamllint flag these.
# Install yamllint for YAML linting
pip install yamllint
# Lint a file
yamllint config.yaml
# Lint with custom rules
yamllint -d "{extends: default, rules: {line-length: {max: 120}}}" docker-compose.yml
Related conversions
Frequent conversions across the catalogue: