Hvis du arbeider med YAML i Python, bruker du nesten helt sikkert PyYAML. Det er standardbiblioteket, det har eksistert siden 2006 og leveres med en funksjon kalt yaml.load() som har en kritisk sikkerhetssårbarhet som har skadet mange team. Løsningen er ett ord — safe_load — men du må forstå hvorfor, hva du kompromitterer med, og når det nyere ruamel.yaml-biblioteket er det bedre valget.

Denne guiden dekker praktisk YAML-parsing i Python: sikker innlasting, strømmer med flere dokumenter, dumping av Python-objekter tilbake til YAML, konfigfil-mønstre med standardverdier og feilhåndtering. Alle eksempler bruker virkelige scenarier — ingen plassholder-data.

Installasjon

bash
pip install pyyaml

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

yaml.safe_load() — Den Du Alltid Bør Bruke

Det viktigste å vite om PyYAML er at yaml.load() kan kjøre vilkårlig Python-kode innebygd i en YAML-fil. Dette er ikke en teoretisk risiko — det er en veldokumentert angrepsvektor. Bruk alltid 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"]
Sikkerhetsnotat: yaml.safe_load() støtter bare standard YAML-typer: strenger, tall, boolske verdier, null, lister og ordbøker. Det kaster en ConstructorError hvis YAML inneholder Python-spesifikke tagger som !!python/object. Dette er nøyaktig den oppførselen du ønsker. yaml.full_load() er tryggere enn den gamle bare yaml.load() men fortsatt mindre restriktiv enn safe_load(). Start med safe_load() og oppgrader bare hvis du virkelig trenger det.

Laste inn en YAML-konfigfil

Her er et realistisk konfigurasjonsinnlastingsmønster for en webapplikasjon. Vi laster inn en YAML-konfigfil og bruker Pythons ordbok-sammenslåing for å fylle inn standardverdier for alt som ikke er angitt:

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)

Dumpe Python-objekter til YAML

yaml.dump() serialiserer Python-ordbøker, lister, strenger, tall, boolske verdier og None til YAML. Som standard brukes flytstil (inline-krøllparenteser) — sett default_flow_style=False for den lesbare blokkstilen:

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)

Strømmer med Flere Dokumenter med load_all

YAML støtter flere dokumenter i en enkelt fil, adskilt av ---. Dette er vanlig i Kubernetes-manifester der én enkelt fil kan inneholde en Deployment, en Service og en ConfigMap. Bruk yaml.safe_load_all() for å iterere over alle dokumenter:

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

Du kan også skrive flere dokumenter til en strøm med 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 — Når Du Trenger å Bevare Kommentarer

PyYAML har én vesentlig begrensning: det fjerner kommentarer ved innlasting. Hvis du laster inn en YAML-fil, endrer den og skriver den tilbake, er alle kommentarer borte. For konfigfiler som mennesker vedlikeholder, er tapet av kommentarer et dealbreaker.

ruamel.yaml implementerer en round-trip-parser som bevarer kommentarer, nøkkelrekkefølge og formatering — det støtter som standard YAML 1.2-spesifikasjonen. Det er riktig valg når du programmatisk redigerer YAML som mennesker vil lese etterpå:

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
  • Bruk PyYAML når du leser YAML for forbruk — parsing av konfigurasjon til appen din, innlasting av testfixtures, behandling av Kubernetes-manifester programmatisk.
  • Bruk ruamel.yaml når du redigerer YAML som mennesker vedlikeholder — oppdatering av konfigfiler på stedet, verktøy som endrer CI-konfigurasjoner, alt der tap av kommentarer ville vært problematisk.
  • ruamel.yaml er også YAML 1.2-kompatibelt som standard, noe som betyr at Norge-problemet (NOfalse) ikke påvirker det. PyYAML bruker YAML 1.1 som standard.

Feilhåndtering

YAML-parseringsfeil kaster yaml.YAMLError, som er basisklassen for alle PyYAML-unntak. Fang alltid det når du laster inn YAML fra upålitelige eller brukerleverte kilder:

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
Valider strukturen etter innlasting. yaml.safe_load() garanterer bare gyldig YAML-syntaks — det validerer ikke dataformens form. En konfigfil med database: øverst er fin; en konfigfil med en liste øverst er også gyldig YAML. Legg til type- og strukturkontroller etter innlasting, eller bruk et skjemavalideringsbibliotek som Pydantic for å parse den innlastede ordboken til en typet modell.

Oppsummering

PyYAML dekker det store flertallet av YAML-arbeid i Python: bruk alltid yaml.safe_load() (ikke yaml.load()), bruk yaml.safe_load_all() for strømmer med flere dokumenter, og bruk yaml.dump() med default_flow_style=False for lesbar utdata. Når du trenger å bevare kommentarer eller få YAML 1.2-semantikk, bytt til ruamel.yaml — det er en enkel oppgradering for lesing og en liten API-endring for skriving. For syntaksfeil før koden din kjører, YAML-validatoren forteller deg nøyaktig hvilken linje og kolonne som er ødelagt.