Als je met YAML werkt in Python, gebruik je vrijwel zeker PyYAML. Het is de standaardbibliotheek,
bestaat al sinds 2006 en bevat een functie genaamd yaml.load() die een kritieke
beveiligingskwetsbaarheid heeft die al veel teams in de problemen heeft gebracht. De oplossing is één woord — safe_load — maar je moet begrijpen waarom, wat je inlevert, en wanneer de nieuwere ruamel.yaml-bibliotheek de betere keuze is.
Deze gids behandelt praktisch YAML-parseren in Python: veilig laden, multi-document streams, Python-objecten terug naar YAML schrijven, config-bestandspatronen met standaardwaarden en foutafhandeling. Alle voorbeelden gebruiken realistische scenario's — geen placeholderdata.
Installatie
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — De Enige die Je Altijd Moet Gebruiken
Het belangrijkste dat je moet weten over
PyYAML
is dat yaml.load() willekeurige Python-code kan uitvoeren die in een YAML-bestand is ingesloten.
Dit is geen theoretisch risico — het is een goed gedocumenteerde aanvalsvector. Gebruik altijd yaml.safe_load():
import yaml
# DANGEROUS — never use this with untrusted input
data = yaml.load(open('config.yaml'), Loader=yaml.FullLoader)
# SAFE — use this for any YAML from external sources
data = yaml.safe_load(open('config.yaml'))
# The attack: a YAML file could contain this, which executes Python
# !!python/object/apply:os.system ["rm -rf /important-dir"]yaml.safe_load() ondersteunt alleen standaard YAML-types:
strings, getallen, booleans, null, lijsten en dicts. Het gooit een ConstructorError als de YAML
Python-specifieke tags bevat zoals !!python/object. Dit is precies het gedrag dat je wilt.
yaml.full_load() is veiliger dan het oude yaml.load() zonder argumenten, maar is nog steeds minder restrictief
dan safe_load(). Begin met safe_load() en upgrade alleen als je het echt nodig hebt.Een YAML-configuratiebestand Laden
Hier is een realistisch patroon voor het laden van een configuratie in een webapplicatie. We laden een YAML-configuratiebestand en gebruiken Python's dict-merge om standaardwaarden in te vullen voor alles wat niet is opgegeven:
# config.yaml
database:
host: postgres.internal
port: 5432
name: myapp_prod
pool_size: 10
redis:
host: redis.internal
port: 6379
logging:
level: INFO
format: jsonimport yaml
from pathlib import Path
from typing import Any
DEFAULT_CONFIG = {
'database': {
'host': 'localhost',
'port': 5432,
'name': 'myapp',
'pool_size': 5,
'ssl': False,
},
'redis': {
'host': 'localhost',
'port': 6379,
'db': 0,
},
'logging': {
'level': 'DEBUG',
'format': 'text',
}
}
def deep_merge(base: dict, override: dict) -> dict:
"""Recursively merge override into base, returning a new dict."""
result = base.copy()
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = value
return result
def load_config(config_path: str | Path) -> dict[str, Any]:
path = Path(config_path)
if not path.exists():
raise FileNotFoundError(f"Config file not found: {path}")
with path.open('r', encoding='utf-8') as f:
raw = yaml.safe_load(f)
if raw is None:
return DEFAULT_CONFIG.copy()
return deep_merge(DEFAULT_CONFIG, raw)
config = load_config('config.yaml')
print(config['database']['host']) # postgres.internal
print(config['database']['ssl']) # False (from defaults)
print(config['redis']['db']) # 0 (from defaults)Python-objecten naar YAML Serialiseren
yaml.dump() serialiseert Python-dicts, lijsten, strings, getallen, booleans en None naar YAML.
Standaard gebruikt het de flow-stijl (inline accolades) — stel default_flow_style=False in voor de
leesbare blokstijl:
import yaml
from dataclasses import dataclass, asdict
@dataclass
class ServiceConfig:
name: str
replicas: int
image: str
port: int
tags: list[str]
service = ServiceConfig(
name='payment-api',
replicas=3,
image='payment-api:2.4.1',
port=8080,
tags=['payments', 'backend', 'critical']
)
# Convert dataclass to dict first, then dump to YAML
output = yaml.dump(
asdict(service),
default_flow_style=False,
sort_keys=False, # preserve insertion order
allow_unicode=True
)
print(output)
# image: payment-api:2.4.1
# name: payment-api
# port: 8080
# replicas: 3
# tags:
# - payments
# - backend
# - critical
# Write to file
with open('service-config.yaml', 'w', encoding='utf-8') as f:
yaml.dump(asdict(service), f, default_flow_style=False, sort_keys=False)Multi-document Streams met load_all
YAML ondersteunt meerdere documenten in één bestand, gescheiden door ---. Dit komt veel voor in
Kubernetes-manifesten waarbij één bestand een Deployment, een Service en een ConfigMap kan bevatten.
Gebruik yaml.safe_load_all() om over alle documenten te itereren:
import yaml
# manifests.yaml contains multiple Kubernetes resources separated by ---
with open('manifests.yaml', 'r') as f:
# safe_load_all returns a generator
documents = list(yaml.safe_load_all(f))
for doc in documents:
if doc is None:
continue
kind = doc.get('kind', 'Unknown')
name = doc.get('metadata', {}).get('name', 'unnamed')
print(f"{kind}: {name}")
# Deployment: payment-api
# Service: payment-api-svc
# ConfigMap: payment-api-configJe kunt ook meerdere documenten naar een stream schrijven met yaml.dump_all():
import yaml
documents = [
{'kind': 'Deployment', 'metadata': {'name': 'api'}, 'spec': {'replicas': 2}},
{'kind': 'Service', 'metadata': {'name': 'api-svc'}, 'spec': {'port': 80}},
]
output = yaml.dump_all(documents, default_flow_style=False)
print(output)
# kind: Deployment
# metadata:
# name: api
# spec:
# replicas: 2
# ---
# kind: Service
# metadata:
# name: api-svc
# spec:
# port: 80ruamel.yaml — Als je Commentaar Wilt Bewaren
PyYAML heeft één significante beperking: het verwijdert commentaar bij het laden. Als je een YAML-bestand laadt, wijzigt en terugschrijft, is al het commentaar verdwenen. Voor configuratiebestanden die mensen onderhouden, is het verliezen van commentaar een dealbreaker.
ruamel.yaml implementeert een round-trip-parser die commentaar, sleutelvolgorde en opmaak behoudt — het richt zich standaard op de YAML 1.2-specificatie. Het is de juiste keuze wanneer je YAML programmatisch bewerkt dat mensen daarna zullen lezen:
from ruamel.yaml import YAML
yaml = YAML()
yaml.preserve_quotes = True
# This config.yaml has important comments we need to keep:
# database:
# host: localhost # change this for production
# port: 5432 # default PostgreSQL port
# pool_size: 5 # increase under heavy load
with open('config.yaml', 'r') as f:
config = yaml.load(f)
# Modify a value
config['database']['host'] = 'postgres.prod.internal'
config['database']['pool_size'] = 20
# Write back — comments and formatting are preserved!
with open('config.yaml', 'w') as f:
yaml.dump(config, f)
# Result:
# database:
# host: postgres.prod.internal # change this for production
# port: 5432 # default PostgreSQL port
# pool_size: 20 # increase under heavy load- Gebruik PyYAML wanneer je YAML leest voor verwerking — configuratie inladen in je app, testfixtures laden, Kubernetes-manifesten programmatisch verwerken.
- Gebruik ruamel.yaml wanneer je YAML bewerkt dat mensen onderhouden — configuratiebestanden in-place bijwerken, tooling die CI-configuraties wijzigt, alles waarbij het verliezen van commentaar een probleem zou zijn.
- ruamel.yaml is ook standaard YAML 1.2-conform, wat betekent dat het Norway Problem (
NO→false) het niet treft. PyYAML gebruikt standaard YAML 1.1.
Foutafhandeling
YAML-parseerfouten gooien yaml.YAMLError, de basisklasse voor alle PyYAML-uitzonderingen.
Vang het altijd op bij het laden van YAML uit niet-vertrouwde of door de gebruiker aangeleverde bronnen:
import yaml
from pathlib import Path
def load_user_config(path: str) -> dict:
try:
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except FileNotFoundError:
raise FileNotFoundError(f"Config file not found: {path}")
except yaml.scanner.ScannerError as e:
# Includes line/column info in the error message
raise ValueError(f"YAML syntax error in {path}:\n{e}")
except yaml.YAMLError as e:
raise ValueError(f"Invalid YAML in {path}: {e}")
if data is None:
return {}
if not isinstance(data, dict):
raise TypeError(f"Expected a YAML mapping at top level, got {type(data).__name__}")
return datayaml.safe_load() garandeert alleen
geldige YAML-syntaxis — het valideert de vorm van de data niet. Een configuratiebestand met database:
bovenaan is prima; een configuratiebestand met een lijst bovenaan is ook geldige YAML. Voeg type- en structuurcontroles
toe na het laden, of gebruik een schemavalidatiebibliotheek zoals
Pydantic om de geladen dict
te parseren naar een getypt model.Samenvatting
PyYAML dekt de overgrote meerderheid van YAML-werk in Python: gebruik altijd yaml.safe_load()
(niet yaml.load()), gebruik yaml.safe_load_all() voor multi-document streams,
en gebruik yaml.dump() met default_flow_style=False voor leesbare uitvoer.
Wanneer je commentaar moet bewaren of YAML 1.2-semantiek nodig hebt, schakel dan over naar ruamel.yaml — het is een directe
upgrade voor lezen en een kleine API-wijziging voor schrijven. Voor syntaxfouten voordat je code überhaupt draait,
vertelt de YAML Validator je precies welke regel en kolom kapot is.