Si vous travaillez avec YAML en Python, vous utilisez presque certainement PyYAML. C'est la bibliothèque standard,
elle existe depuis 2006, et elle inclut une fonction appelée yaml.load() qui contient une faille de sécurité critique
ayant déjà causé de gros problèmes à beaucoup d'équipes. Le correctif tient en un mot — safe_load — mais vous
devez comprendre pourquoi, ce que vous sacrifiez, et quand la bibliothèque plus récente ruamel.yaml est le meilleur choix.
Ce guide couvre les cas pratiques du parsing YAML en Python : le chargement sécurisé, les flux multi-documents, la sérialisation d'objets Python en YAML, les patterns de fichiers de configuration avec valeurs par défaut, et la gestion des erreurs. Tous les exemples sont tirés de scénarios réels — pas de données fictives.
Installation
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — Celle que vous devriez toujours utiliser
La chose la plus importante à savoir sur
PyYAML
est que yaml.load() peut exécuter du code Python arbitraire intégré dans un fichier YAML.
Ce n'est pas un risque théorique — c'est un vecteur d'attaque bien documenté. Utilisez toujours 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() ne prend en charge que les types YAML standard :
chaînes, nombres, booléens, null, listes et dictionnaires. Elle lèvera une ConstructorError si le YAML
contient des balises spécifiques à Python comme !!python/object. C'est exactement le comportement souhaité.
yaml.full_load() est plus sûre que l'ancienne yaml.load() nue, mais reste moins restrictive
que safe_load(). Commencez par safe_load() et n'évoluez que si vous en avez réellement besoin.Charger un fichier de configuration YAML
Voici un pattern réaliste de chargement de configuration pour une application web. On charge un fichier YAML et on utilise une fusion de dictionnaires Python pour combler les valeurs manquantes avec des défauts :
# 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)Sérialiser des objets Python en YAML
yaml.dump() sérialise des dicts, listes, chaînes, nombres, booléens et None Python en YAML.
Par défaut, elle utilise le style flux (accolades inline) — définissez default_flow_style=False pour obtenir
le style bloc plus lisible :
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)Flux multi-documents avec load_all
YAML prend en charge plusieurs documents dans un seul fichier, séparés par ---. C'est courant dans
les manifestes Kubernetes où un seul fichier peut contenir un Deployment, un Service et un ConfigMap.
Utilisez yaml.safe_load_all() pour itérer sur tous les documents :
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-configVous pouvez aussi écrire plusieurs documents dans un flux avec 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 — Quand vous avez besoin de préserver les commentaires
PyYAML a une limitation importante : elle supprime les commentaires lors du chargement. Si vous chargez un fichier YAML, le modifiez, puis le réécrivez, tous les commentaires disparaissent. Pour les fichiers de configuration maintenus par des humains, perdre les commentaires est rédhibitoire.
ruamel.yaml implémente un parseur aller-retour qui préserve les commentaires, l'ordre des clés et la mise en forme — elle cible la spécification YAML 1.2 par défaut. C'est le bon choix dès que vous modifiez programmatiquement du YAML que des humains liront ensuite :
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- Utilisez PyYAML quand vous lisez du YAML pour le consommer — parser une config dans votre appli, charger des fixtures de test, traiter des manifestes Kubernetes programmatiquement.
- Utilisez ruamel.yaml quand vous éditez du YAML maintenu par des humains — mettre à jour des fichiers de config en place, des outils qui modifient des configs CI, tout ce où perdre les commentaires serait problématique.
- ruamel.yaml est aussi conforme à YAML 1.2 par défaut, ce qui signifie que le problème Norway (
NO→false) ne vous affecte pas. PyYAML utilise YAML 1.1 par défaut.
Gestion des erreurs
Les erreurs de parsing YAML lèvent yaml.YAMLError, qui est la classe de base de toutes les exceptions PyYAML.
Interceptez-la toujours lors du chargement de YAML provenant de sources non fiables ou fournies par l'utilisateur :
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() garantit uniquement
la syntaxe YAML valide — elle ne valide pas la forme des données. Un fichier de config avec database:
au niveau supérieur est correct ; un fichier de config avec une liste au niveau supérieur est aussi du YAML valide. Ajoutez des vérifications de type et de structure
après le chargement, ou utilisez une bibliothèque de validation de schéma comme
Pydantic pour analyser le dict chargé
en un modèle typé.Conclusion
PyYAML couvre la grande majorité du travail YAML en Python : utilisez toujours yaml.safe_load()
(et non yaml.load()), utilisez yaml.safe_load_all() pour les flux multi-documents,
et utilisez yaml.dump() avec default_flow_style=False pour une sortie lisible.
Quand vous avez besoin de préserver les commentaires ou d'obtenir la sémantique YAML 1.2, passez à ruamel.yaml — c'est une mise à niveau transparente pour la lecture et un léger changement d'API pour l'écriture. Pour les erreurs de syntaxe avant même l'exécution de votre code,
le Validateur YAML vous indiquera exactement quelle ligne et quelle colonne est défectueuse.