Se stai lavorando con YAML in Python, quasi certamente stai usando PyYAML. È la libreria standard,
esiste dal 2006 e include una funzione chiamata yaml.load() che ha una vulnerabilità di sicurezza critica
che ha colpito molti team. La soluzione è una parola sola — safe_load — ma devi capire perché,
cosa si perde, e quando la libreria più recente ruamel.yaml è la scelta migliore.
Questa guida copre il parsing YAML pratico in Python: caricamento sicuro, stream multi-documento, serializzazione di oggetti Python in YAML, pattern per file di configurazione con valori predefiniti e gestione degli errori. Tutti gli esempi usano scenari reali — niente dati segnaposto.
Installazione
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — Quello che Dovresti Sempre Usare
La cosa più importante da sapere su
PyYAML
è che yaml.load() può eseguire codice Python arbitrario incorporato in un file YAML.
Non è un rischio teorico — è un vettore di attacco ben documentato. Usa sempre 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() supporta solo i tipi YAML standard:
stringhe, numeri, booleani, null, liste e dizionari. Solleverà un ConstructorError se il YAML
contiene tag specifici di Python come !!python/object. Questo è esattamente il comportamento che desideri.
yaml.full_load() è più sicuro del vecchio yaml.load() senza argomenti, ma è comunque meno restrittivo
di safe_load(). Parti da safe_load() e passa a qualcosa di più permissivo solo se ne hai davvero bisogno.Caricare un File di Configurazione YAML
Ecco un pattern realistico per caricare una configurazione in un'applicazione web. Carichiamo un file YAML e usiamo il merge dei dizionari Python per applicare i valori predefiniti a tutto ciò che non è specificato:
# 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)Serializzare Oggetti Python in YAML
yaml.dump() serializza dizionari, liste, stringhe, numeri, booleani e None di Python in YAML.
Per impostazione predefinita usa lo stile a flusso (parentesi graffe inline) — imposta default_flow_style=False
per ottenere lo stile a blocchi leggibile:
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)Stream Multi-Documento con load_all
YAML supporta più documenti in un singolo file, separati da ---. Questo è comune nei
manifest Kubernetes dove un singolo file può contenere un Deployment, un Service e una ConfigMap.
Usa yaml.safe_load_all() per iterare su tutti i documenti:
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-configPuoi anche scrivere più documenti in uno stream con 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 — Quando Devi Preservare i Commenti
PyYAML ha un limite significativo: rimuove i commenti durante il caricamento. Se carichi un file YAML, lo modifichi e lo riscrivi, tutti i commenti spariscono. Per i file di configurazione gestiti da esseri umani, perdere i commenti è un problema insuperabile.
ruamel.yaml implementa un parser round-trip che preserva commenti, ordine delle chiavi e formattazione — punta allo spec YAML 1.2 per impostazione predefinita. È la scelta giusta ogni volta che modifichi programmaticamente YAML che gli esseri umani leggeranno in seguito:
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- Usa PyYAML quando leggi YAML per consumarlo — parsing della configurazione nella tua app, caricamento di fixture di test, elaborazione programmatica di manifest Kubernetes.
- Usa ruamel.yaml quando modifichi YAML gestito da esseri umani — aggiornamento di file di configurazione in-place, tooling che modifica configurazioni CI, qualsiasi situazione dove perdere i commenti sarebbe problematico.
- ruamel.yaml è anche conforme a YAML 1.2 per impostazione predefinita, il che significa che il Norway Problem (
NO→false) non lo riguarda. PyYAML usa YAML 1.1 per impostazione predefinita.
Gestione degli Errori
Gli errori di parsing YAML sollevano yaml.YAMLError, che è la classe base per tutte le eccezioni PyYAML.
Intercettalo sempre quando carichi YAML da fonti non attendibili o fornite dall'utente:
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() garantisce solo
la sintassi YAML valida — non valida la forma dei dati. Un file di configurazione con database:
in cima va bene; anche un file di configurazione con una lista in cima è YAML valido. Aggiungi controlli di tipo
e struttura dopo il caricamento, oppure usa una libreria di validazione dello schema come
Pydantic per fare il parsing del dizionario
caricato in un modello tipizzato.Conclusione
PyYAML copre la stragrande maggioranza del lavoro YAML in Python: usa sempre yaml.safe_load()
(non yaml.load()), usa yaml.safe_load_all() per gli stream multi-documento
e usa yaml.dump() con default_flow_style=False per un output leggibile.
Quando hai bisogno di preservare i commenti o ottenere la semantica YAML 1.2, passa a ruamel.yaml — è un upgrade
immediato per la lettura e una piccola modifica dell'API per la scrittura. Per gli errori di sintassi prima ancora
che il codice venga eseguito, il Validatore YAML ti dirà esattamente quale riga e colonna è errata.