YAML #

YAML (YAML Ain’t Markup Language) is a data serialization format designed to be human-readable. Unlike JSON with its curly braces and quotes, YAML relies on indentation and colons — the result is much cleaner for configuration files. You’ll find YAML everywhere: docker-compose.yml, GitHub Actions, Kubernetes manifests, Ansible playbooks, and Python app configuration for things like Django and FastAPI. Understanding YAML well — including its traps, especially security ones — is an important skill for every Python developer.

Installation #

PyYAML is a third-party library that needs to be installed first:

pip install pyyaml
For high-performance needs (parsing large YAML files), consider ruamel.yaml as a more complete alternative, or pyyaml with the C extension that’s installed automatically on most systems.

Basic YAML Syntax #

Before reading or writing YAML from Python, it’s important to understand the YAML structure itself. YAML uses indentation (spaces, not tabs) to define hierarchy.

# Comments start with the # sign

# --- Scalar data types ---
nama: Budi Santoso
usia: 28
tinggi: 175.5
aktif: true          # boolean: true/false (not True/False like Python)
tidak_ada: null      # null or ~

# --- Multi-line strings ---
deskripsi: |          # literal block -- newlines are preserved
  Baris pertama.
  Baris kedua.
  Baris ketiga.

ringkasan: >          # folded block -- newlines become spaces
  Ini semua akan
  jadi satu baris
  panjang.

# --- List ---
hobi:
  - membaca
  - hiking
  - coding

# Or inline format (like a JSON array)
tag: [python, backend, api]

# --- Dict/Mapping ---
alamat:
  kota: Bandung
  provinsi: Jawa Barat
  kode_pos: "40115"    # string -- quotes for numbers that need to stay strings

# --- List of dict ---
tim:
  - nama: Budi
    peran: backend
  - nama: Sari
    peran: frontend

Reading YAML #

PyYAML provides two main functions for reading: safe_load() and load(). The choice between them isn’t about preference — it’s about security.

import yaml

# safe_load() -- from a string
yaml_string = """
nama: Budi
usia: 28
hobi:
  - membaca
  - coding
aktif: true
"""

data = yaml.safe_load(yaml_string)
print(data)
# {'nama': 'Budi', 'usia': 28, 'hobi': ['membaca', 'coding'], 'aktif': True}

print(type(data["aktif"]))  # <class 'bool'>
print(type(data["hobi"]))   # <class 'list'>

# safe_load() -- from a file
with open("config.yaml", "r", encoding="utf-8") as f:
    config = yaml.safe_load(f)

Don’t use yaml.load() without a Loader on input you don’t control. yaml.load() without a Loader (or with Loader=yaml.Loader) can execute arbitrary Python code embedded in YAML — this is a serious security vulnerability (arbitrary code execution). Always use yaml.safe_load() for external data, or at minimum yaml.load(..., Loader=yaml.SafeLoader).

# ANTI-PATTERN: vulnerable to arbitrary code execution
data = yaml.load(user_input)  # ✗ -- DANGEROUS

# CORRECT: use safe_load for external input
data = yaml.safe_load(user_input)  # ✓

Writing YAML #

yaml.dump() converts Python objects into YAML strings, with various formatting options.

import yaml

data = {
    "nama": "Budi Santoso",
    "usia": 28,
    "hobi": ["membaca", "hiking"],
    "alamat": {
        "kota": "Bandung",
        "provinsi": "Jawa Barat"
    },
    "aktif": True,
    "catatan": None
}

# Default output (keys sorted alphabetically, ASCII only)
print(yaml.dump(data))
# aktif: true
# alamat:
#   kota: Bandung
#   provinsi: Jawa Barat
# usia: 28
# ...

# More readable output
yaml_output = yaml.dump(
    data,
    allow_unicode=True,    # non-ASCII characters aren't escaped
    default_flow_style=False,  # block format, not inline
    sort_keys=False,       # key order preserved
    indent=2
)
print(yaml_output)

# Directly to a file
with open("output.yaml", "w", encoding="utf-8") as f:
    yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
# ANTI-PATTERN: dumping without allow_unicode
yaml.dump({"kota": "Jëpara"})
# kota: "J\xebpara"  ✗ -- non-ASCII characters escaped, hard to read

# CORRECT: enable allow_unicode
yaml.dump({"kota": "Jëpara"}, allow_unicode=True)
# kota: Jëpara  ✓

Anchors and Aliases #

Anchors (&) and aliases (*) are YAML features that let you define a value once and reference it in several places — very useful in config files to avoid duplication.

# Example: config.yaml with anchors
defaults: &defaults
  timeout: 30
  retry: 3
  log_level: INFO

database:
  <<: *defaults          # merge key -- inherits everything from defaults
  host: localhost
  port: 5432
  name: myapp

cache:
  <<: *defaults          # also inherits from defaults
  host: localhost
  port: 6379
  timeout: 5             # overrides the timeout from defaults
import yaml

with open("config.yaml", "r", encoding="utf-8") as f:
    config = yaml.safe_load(f)

# PyYAML automatically resolves anchors and aliases
print(config["database"]["timeout"])  # 30 (from defaults)
print(config["cache"]["timeout"])     # 5 (overridden)
print(config["cache"]["retry"])       # 3 (from defaults)

Multi-Document YAML #

A single YAML file can contain several documents separated by ---. This is commonly used in Kubernetes manifests and pipeline configurations.

# multi.yaml
---
nama: Budi
peran: backend
---
nama: Sari
peran: frontend
---
nama: Andi
peran: devops
import yaml

# Read all documents at once
with open("multi.yaml", "r", encoding="utf-8") as f:
    doc_list = list(yaml.safe_load_all(f))

print(len(doc_list))         # 3
print(doc_list[0]["nama"])   # Budi
print(doc_list[2]["peran"])  # devops

# Write several documents
team = [
    {"nama": "Budi", "peran": "backend"},
    {"nama": "Sari", "peran": "frontend"},
]

with open("team.yaml", "w", encoding="utf-8") as f:
    yaml.dump_all(team, f, allow_unicode=True, default_flow_style=False)

Custom Objects #

To serialize and deserialize custom Python classes, PyYAML uses the representer and constructor mechanisms.

import yaml
from dataclasses import dataclass

@dataclass
class Konfigurasi:
    host: str
    port: int
    debug: bool = False

# Representer: Python object → YAML
def konfigurasi_representer(dumper, data):
    return dumper.represent_mapping("!Konfigurasi", {
        "host": data.host,
        "port": data.port,
        "debug": data.debug
    })

# Constructor: YAML → Python object
def konfigurasi_constructor(loader, node):
    nilai = loader.construct_mapping(node)
    return Konfigurasi(**nilai)

yaml.add_representer(Konfigurasi, konfigurasi_representer)
yaml.add_constructor("!Konfigurasi", konfigurasi_constructor)

# Serialize
cfg = Konfigurasi(host="localhost", port=8080, debug=True)
yaml_string = yaml.dump(cfg)
print(yaml_string)
# !Konfigurasi
# debug: true
# host: localhost
# port: 8080

# Deserialize
cfg2 = yaml.load(yaml_string, Loader=yaml.FullLoader)
print(cfg2)          # Konfigurasi(host='localhost', port=8080, debug=True)
print(type(cfg2))    # <class 'Konfigurasi'>

Application Configuration Patterns #

YAML is a top choice for application config files because of its readability. Here’s a commonly used pattern.

# config.yaml
app:
  nama: MyApp
  versi: "1.0.0"
  debug: false
  secret_key: "ganti-dengan-nilai-aman"

server:
  host: "0.0.0.0"
  port: 8000
  workers: 4

database:
  host: localhost
  port: 5432
  nama: myapp_db
  user: admin
  password: ""          # leave empty, fill via environment variable

logging:
  level: INFO
  format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
import yaml
import os
from pathlib import Path
from dataclasses import dataclass

def load_config(path: str = "config.yaml") -> dict:
    config_path = Path(path)
    
    if not config_path.exists():
        raise FileNotFoundError(f"Config file not found: {path}")
    
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)
    
    if config is None:
        raise ValueError("Config file is empty or invalid")
    
    # Override sensitive values from environment variables
    if os.getenv("DB_PASSWORD"):
        config["database"]["password"] = os.getenv("DB_PASSWORD")
    
    if os.getenv("SECRET_KEY"):
        config["app"]["secret_key"] = os.getenv("SECRET_KEY")
    
    return config

config = load_config()
print(config["app"]["nama"])       # MyApp
print(config["server"]["port"])    # 8000

YAML vs JSON — When to Choose #

Choose YAML when:
  ✓ Configuration files read/edited by humans (docker-compose, CI/CD)
  ✓ You need comments in the file (JSON doesn't support comments)
  ✓ You need anchors/aliases to avoid duplication
  ✓ Deep hierarchical structures needing high readability

Choose JSON when:
  ✓ API responses / inter-service communication
  ✓ Machine-processed data, readability isn't a priority
  ✓ Parsing performance matters more (JSON parses faster)
  ✓ Ecosystems already using JSON (package.json, tsconfig.json)
  ✓ You need easy schema validation (JSON Schema is more mature)

Summary #

  • Always safe_load() — use yaml.safe_load() for all external input; yaml.load() without the right Loader is a serious security hole.
  • allow_unicode=True — enable it when calling dump() so non-ASCII characters aren’t escaped into \uXXXX.
  • default_flow_style=False — use it so YAML output is block-styled (hierarchical), not inline like JSON.
  • sort_keys=False — preserve the original key order during serialization if order matters.
  • Anchors (&) and aliases (*) — leverage them to avoid duplication in complex config files.
  • safe_load_all() and dump_all() — for reading and writing YAML files containing multiple documents.
  • Custom representers/constructors — use yaml.add_representer() and yaml.add_constructor() to serialize/deserialize custom classes.
  • YAML for config, JSON for APIs — YAML excels at readability and features (comments, anchors); JSON excels at performance and cross-platform compatibility.

← Previous: JSON   Next: MySQL →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact