Om du arbetar med YAML i Python använder du nästan säkert PyYAML. Det är standardbiblioteket,
det har funnits sedan 2006 och levereras med en funktion kallad yaml.load() som har en kritisk
säkerhetssårbarhet som skadat många team. Lösningen är ett ord — safe_load — men du
behöver förstå varför, vad du kompromissar med och när det nyare ruamel.yaml-biblioteket är det bättre valet.
Den här guiden täcker praktisk YAML-parsning i Python: säker inläsning, strömmar med flera dokument, dumpning av Python-objekt tillbaka till YAML, konfigfilsmönster med standardvärden och felhantering. Alla exempel använder verkliga scenarier — inga platshållardata.
Installation
pip install pyyaml
# For ruamel.yaml (covered later)
pip install ruamel.yamlyaml.safe_load() — Den Du Alltid Bör Använda
Det viktigaste att veta om
PyYAML
är att yaml.load() kan exekvera godtycklig Python-kod inbäddad i en YAML-fil.
Det här är inte en teoretisk risk — det är en väldokumenterad attackvektor. Använd alltid 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() stöder bara standardiserade YAML-typer:
strängar, tal, booleska värden, null, listor och ordlistor. Det kastar ett ConstructorError om YAML
innehåller Python-specifika taggar som !!python/object. Det är precis det beteendet du vill ha.
yaml.full_load() är säkrare än den gamla blotta yaml.load() men fortfarande mindre restriktiv
än safe_load(). Börja med safe_load() och uppgradera bara om du verkligen behöver det.Läsa in en YAML-konfigfil
Här är ett realistiskt mönster för konfigurationsinläsning för en webbapplikation. Vi läser in en YAML-konfigfil och använder Pythons ordlistsammanslagning för att fylla i standardvärden för allt som inte angetts:
# 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)Dumpa Python-objekt till YAML
yaml.dump() serialiserar Python-ordlistor, listor, strängar, tal, booleska värden och None till YAML.
Som standard används flödesstil (inline-klammerparenteser) — sätt default_flow_style=False för det
läsbara blockstilen:
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)Strömmar med Flera Dokument med load_all
YAML stöder flera dokument i en enda fil, separerade av ---. Det här är vanligt i
Kubernetes-manifest där en enda fil kan innehålla en Deployment, en Service och en ConfigMap.
Använd yaml.safe_load_all() för att iterera över alla dokument:
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-configDu kan också skriva flera dokument till en ström med 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 — När Du Behöver Bevara Kommentarer
PyYAML har en betydande begränsning: det tar bort kommentarer vid inläsning. Om du läser in en YAML-fil, modifierar den och skriver tillbaka, är alla kommentarer borta. För konfigfiler som människor underhåller är förlusten av kommentarer ett dealbreaker.
ruamel.yaml implementerar en round-trip-parser som bevarar kommentarer, nyckelordning och formatering — det riktar sig mot YAML 1.2-specifikationen som standard. Det är rätt val när du programmatiskt redigerar YAML som människor kommer att läsa efteråt:
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- Använd PyYAML när du läser YAML för konsumtion — parsning av konfiguration till din app, inläsning av testfixtures, bearbetning av Kubernetes-manifest programmatiskt.
- Använd ruamel.yaml när du redigerar YAML som människor underhåller — uppdatering av konfigfiler på plats, verktyg som ändrar CI-konfigurationer, allt där förlust av kommentarer vore problematiskt.
- ruamel.yaml är också YAML 1.2-kompatibelt som standard, vilket betyder att Norgeproblemet (
NO→false) inte påverkar det. PyYAML använder YAML 1.1 som standard.
Felhantering
YAML-parsningsfel kastar yaml.YAMLError, som är basklassen för alla PyYAML-undantag.
Fånga alltid det när du laddar YAML från otillförlitliga eller användargivna källor:
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() garanterar bara
giltig YAML-syntax — det validerar inte dataformen. En konfigfil med database:
överst är fine; en konfigfil med en lista överst är också giltig YAML. Lägg till typ- och strukturkontroller
efter inläsning, eller använd ett schemavalideringsbibliotek som
Pydantic för att parsa den inlästa ordlistan
till en typad modell.Sammanfattning
PyYAML täcker den stora majoriteten av YAML-arbete i Python: använd alltid yaml.safe_load()
(inte yaml.load()), använd yaml.safe_load_all() för strömmar med flera dokument,
och använd yaml.dump() med default_flow_style=False för läsbar utdata.
När du behöver bevara kommentarer eller få YAML 1.2-semantik, byt till ruamel.yaml — det är en enkel
uppgradering för läsning och en liten API-förändring för skrivning. För syntaxfel innan din kod ens kör,
YAML-validatorn berättar exakt vilken rad och kolumn som är trasig.