Python'da YAML ile çalışıyorsanız, büyük ihtimalle PyYAML kullanıyorsunuzdur. Bu standart kütüphane 2006'dan beri var ve kritik bir güvenlik açığı taşıyan yaml.load() adlı bir fonksiyonla geliyor; bu açık pek çok ekibi zor durumda bırakmış. Çözüm tek kelime — safe_load — ama nedenini, ne kaybettiğinizi ve daha yeni ruamel.yaml kütüphanesinin ne zaman daha iyi bir seçim olduğunu anlamanız gerekiyor.

Bu rehber Python'da pratik YAML ayrıştırmayı kapsar: güvenli yükleme, çok belgeli akışlar, Python nesnelerini tekrar YAML'a dönüştürme, varsayılan değerler içeren yapılandırma dosyası kalıpları ve hata yönetimi. Tüm örnekler gerçek dünya senaryolarını kullanır — yer tutucu veri yok.

Kurulum

bash
pip install pyyaml

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

yaml.safe_load() — Her Zaman Kullanmanız Gereken

PyYAML hakkında bilmeniz gereken en önemli şey: yaml.load(), bir YAML dosyasına gömülü rastgele Python kodunu çalıştırabilir. Bu teorik bir risk değil — iyi belgelenmiş bir saldırı vektörü. Her zaman yaml.safe_load() kullanın:

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"]
Güvenlik notu: yaml.safe_load() yalnızca standart YAML tiplerini destekler: string, sayı, boolean, null, liste ve dict. YAML içinde !!python/object gibi Python'a özgü etiketler bulunursa ConstructorError fırlatır. İstediğiniz davranış tam da bu. yaml.full_load(), eski yaml.load()'dan daha güvenlidir ancak safe_load()'dan daha az kısıtlayıcıdır. safe_load() ile başlayın ve gerçekten ihtiyaç duyduğunuzda yükseltin.

Bir YAML Yapılandırma Dosyasını Yükleme

İşte bir web uygulaması için gerçekçi bir yapılandırma yükleme kalıbı. Bir YAML yapılandırma dosyasını yükleyip belirtilmemiş her şey için Python'un dict birleştirmesiyle varsayılan değerleri dolduruyoruz:

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)

Python Nesnelerini YAML'a Dönüştürme

yaml.dump(), Python dict, liste, string, sayı, boolean ve None değerlerini YAML'a dönüştürür. Varsayılan olarak akış stilini (satır içi süslü parantezler) kullanır — okunabilir blok stili için default_flow_style=False ayarlayın:

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)

load_all ile Çok Belgeli Akışlar

YAML, --- ile ayrılmış tek bir dosyada birden fazla belgeyi destekler. Bu, tek bir dosyanın Deployment, Service ve ConfigMap içerebildiği Kubernetes manifest'lerinde yaygındır. Tüm belgeler üzerinde yineleme yapmak için yaml.safe_load_all() kullanın:

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

Ayrıca yaml.dump_all() ile bir akışa birden fazla belge yazabilirsiniz:

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 — Yorumları Korumak İstediğinizde

PyYAML'ın önemli bir sınırlaması var: yükleme sırasında yorumları kaldırıyor. Bir YAML dosyasını yükleyip değiştirip geri yazarsanız tüm yorumlar gidiyor. İnsanların yönettiği yapılandırma dosyaları için yorumların kaybolması kabul edilemez bir durum.

ruamel.yaml, yorumları, anahtar sıralamasını ve biçimlendirmeyi koruyan round-trip bir ayrıştırıcı uygular — varsayılan olarak YAML 1.2 spesifikasyonunu hedefler. İnsanların sonradan okuyacağı YAML'ı programlı olarak düzenlediğinizde bu doğru seçimdir:

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
  • PyYAML kullanın: YAML'ı tüketmek için okurken — yapılandırmayı uygulamanıza ayrıştırırken, test fixture'larını yüklerken, Kubernetes manifest'lerini programlı olarak işlerken.
  • ruamel.yaml kullanın: insanların yönettiği YAML'ı düzenlerken — yapılandırma dosyalarını yerinde güncellerken, CI yapılandırmalarını değiştiren araçlarda, yorumların kaybolmasının sorun yaratacağı her durumda.
  • ruamel.yaml varsayılan olarak YAML 1.2 uyumludur; bu da Norveç Sorununu (NOfalse) etkilemediği anlamına gelir. PyYAML varsayılan olarak YAML 1.1 kullanır.

Hata Yönetimi

YAML ayrıştırma hataları, tüm PyYAML istisnalarının temel sınıfı olan yaml.YAMLError'u fırlatır. Güvenilmeyen veya kullanıcı tarafından sağlanan kaynaklardan YAML yüklerken her zaman yakalayın:

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
Yüklemeden sonra yapıyı doğrulayın. yaml.safe_load() yalnızca geçerli YAML sözdizimini garanti eder — verinin şeklini doğrulamaz. Üstte database: bulunan bir yapılandırma dosyası geçerlidir; üstte liste bulunan bir yapılandırma dosyası da geçerli YAML'dır. Yüklemeden sonra tür ve yapı kontrolleri ekleyin ya da yüklenen dict'i yazımlı bir modele ayrıştırmak için Pydantic gibi bir şema doğrulama kütüphanesi kullanın.

Özet

PyYAML, Python'daki YAML işlerinin büyük çoğunluğunu karşılar: her zaman yaml.safe_load() kullanın (yaml.load() değil), çok belgeli akışlar için yaml.safe_load_all() kullanın ve okunabilir çıktı için yaml.dump()'u default_flow_style=False ile kullanın. Yorumları korumak veya YAML 1.2 semantiği almak istediğinizde ruamel.yaml'a geçin — okuma için doğrudan bir yükseltme, yazma için küçük bir API değişikliğidir. Kodunuz çalışmadan önce sözdizimi hataları için YAML Doğrulayıcı, tam olarak hangi satır ve sütunun bozuk olduğunu söyler.