Python で YAML ファイルを読み書きする(PyYAML)

YAML は設定ファイルや構成管理で広く使われるデータフォーマットだ。JSON と比べてインデントベースで人間が読みやすく、コメントも書ける。Python では PyYAML ライブラリを使って YAML ファイルの読み書きを行う。

インストール

PyYAML は標準ライブラリに含まれていないため、pip でインストールする必要がある。

pip install pyyaml

インストール後は import yaml で使えるようになる。モジュール名がパッケージ名と異なる点に注意しておこう。

YAML ファイルを読み込む

yaml.safe_load() を使うと、YAML 文字列を Python のオブジェクト(辞書やリスト)に変換できる。

import yaml

with open("config.yaml", "r", encoding="utf-8") as f:
    data = yaml.safe_load(f)

print(data)

たとえば次のような YAML ファイルがあるとする。

database:
  host: localhost
  port: 5432
  name: myapp

logging:
  level: DEBUG
  file: app.log

これを safe_load() で読み込むと、ネストされた辞書として取得できる。

# 結果
{
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "myapp"
    },
    "logging": {
        "level": "DEBUG",
        "file": "app.log"
    }
}

YAML の型変換は自動で行われる。文字列はそのまま str に、数値は int や float に、true/false は bool にそれぞれマッピングされる。

safe_load と load の違い

yaml.load() は任意の Python オブジェクトをデシリアライズできるため、悪意ある YAML を読み込むとコード実行の脆弱性が生じる。安全な yaml.safe_load() を使うのが原則だ。

safe_load

基本的な型(辞書、リスト、文字列、数値、真偽値、None)のみを生成する。外部から受け取った YAML を安全に処理できる

load(Loader 指定なし)

任意の Python オブジェクトを構築でき、悪意のある入力でコードが実行される危険がある。明確な理由がない限り使うべきではない

どうしても load() を使う場合は、Loader 引数を明示的に指定する。

# 非推奨だが必要な場合
data = yaml.load(content, Loader=yaml.SafeLoader)

# これは safe_load() と等価

YAML ファイルに書き込む

yaml.dump() で Python オブジェクトを YAML 形式に変換して書き出せる。

import yaml

config = {
    "server": {
        "host": "0.0.0.0",
        "port": 8080,
    },
    "features": ["auth", "logging", "cache"],
}

with open("output.yaml", "w", encoding="utf-8") as f:
    yaml.dump(config, f, default_flow_style=False, allow_unicode=True)

allow_unicode=True を指定しないと、日本語がエスケープされて読みにくくなる。default_flow_style=False はブロックスタイル(インデント形式)での出力を保証するオプションだ。

出力結果は次のようになる。

features:
- auth
- logging
- cache
server:
  host: 0.0.0.0
  port: 8080

複数ドキュメントを扱う

YAML では --- で区切ることで 1 つのファイルに複数のドキュメントを格納できる。読み込みには safe_load_all() を使う。

import yaml

yaml_text = """

name: Alice
role: admin

name: Bob
role: user
“”"

for doc in yaml.safe_load_all(yaml_text):
print(doc)

{‘name’: ‘Alice’, ‘role’: ‘admin’}

{‘name’: ‘Bob’, ‘role’: ‘user’}


書き出す場合は yaml.dump_all() を使えばよい。

docs = [
    {"name": "Alice", "role": "admin"},
    {"name": "Bob", "role": "user"},
]

output = yaml.dump_all(docs, allow_unicode=True)
print(output)

YAML 特有の型変換の罠

YAML の自動型変換は便利だが、意図しない変換が起きることがある。たとえばノルウェーの国コード NO が真偽値 False に変換されるのは有名な問題だ。

import yaml

# "NO" が False に変換される
data = yaml.safe_load("country: NO")
print(data)  # {'country': False}

# クォートで囲めば文字列として扱われる
data = yaml.safe_load('country: "NO"')
print(data)  # {'country': 'NO'}

同様に on / offyes / no も真偽値として解釈される。設定ファイルを書くときは、文字列として扱いたい値をクォートで囲む習慣をつけておくと安全だ。

YAML の自動型変換

“NO”, “on”, “off” などが意図せず bool に変換される

値をクォートで囲んで文字列と明示する

実践的な使い方:設定ファイルの管理

実際のプロジェクトでは、環境ごとに異なる設定を YAML で管理するケースが多い。デフォルト値と環境固有の値をマージするパターンを示す。

import yaml

def load_config(env="development"):
    with open("config/default.yaml", encoding="utf-8") as f:
        default = yaml.safe_load(f)

    try:
        with open(f"config/{env}.yaml", encoding="utf-8") as f:
            override = yaml.safe_load(f)
    except FileNotFoundError:
        override = {}

    # 浅いマージ(ネストが深い場合は再帰的なマージが必要)
    default.update(override)
    return default

config = load_config("production")

ネストされた辞書を再帰的にマージする場合は、手動で実装するか、deepmerge などの外部ライブラリを活用するとよい。PyYAML 自体にはマージ機能が組み込まれていないため、設計段階で設定の階層構造をシンプルに保つことも重要になってくる。