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

bash
pip install pyyaml

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

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

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"]
Säkerhetsnotering: 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:

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)

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:

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

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 också skriva flera dokument till 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 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:

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

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
Validera strukturen efter inläsning. yaml.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.