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

bash
pip install pyyaml

# For ruamel.yaml (covered later)
pip install ruamel.yaml

yaml.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() :

python
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"]
Note de sécurité : 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 :

python
# config.yaml
database:
  host: postgres.internal
  port: 5432
  name: myapp_prod
  pool_size: 10

redis:
  host: redis.internal
  port: 6379

logging:
  level: INFO
  format: json
python
import 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 :

python
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 :

python
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-config

Vous pouvez aussi écrire plusieurs documents dans un flux avec yaml.dump_all() :

python
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: 80

ruamel.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 :

python
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 (NOfalse) 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 :

python
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 data
Validez la structure après le chargement. yaml.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.