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), considerruamel.yamlas a more complete alternative, orpyyamlwith 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 withLoader=yaml.Loader) can execute arbitrary Python code embedded in YAML — this is a serious security vulnerability (arbitrary code execution). Always useyaml.safe_load()for external data, or at minimumyaml.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()— useyaml.safe_load()for all external input;yaml.load()without the right Loader is a serious security hole.allow_unicode=True— enable it when callingdump()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()anddump_all()— for reading and writing YAML files containing multiple documents.- Custom representers/constructors — use
yaml.add_representer()andyaml.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.