Learning Platform
Урок 07.03 · 18 мин
Начальный
TOMLYAMLConfigurationtomllibPyYAML
Airflow: DAG-конфиги и YAML в Variable/Connection GitHub Actions: anatomy — jobs, steps, triggers, YAML

Почему конфиги — отдельный жанр

JSON и CSV — это форматы для данных. Конфиги — другой жанр: их читает чаще человек, чем программа, их редактируют руками, в них хочется писать комментарии и не дублировать значения. Поэтому в Python-мире сложилось два специализированных формата для конфигов: TOML и YAML.

TOML — современный, явный, без сюрпризов. С Python 3.11 он стал частью stdlib (tomllib), и сейчас де-факто стандарт для Python-инструментария: pyproject.toml лежит в каждом проекте.

YAML — наследник 2001 года, мощный, но коварный. Используется во всех «оркестраторских» инструментах: Docker Compose, Kubernetes, Airflow, GitHub Actions, Ansible. Без понимания YAML вы не закроете ни одного PR в инфраструктурный проект.

Junior DE должен уметь читать и писать оба. Начнём с того, что проще и безопаснее.

TOML: явный и предсказуемый

TOML расшифровывается как Tom’s Obvious, Minimal Language — назван по имени автора (Tom Preston-Werner, сооснователь GitHub). Главная философия: минимум сюрпризов. Что написано — то и значит.

# комментарий, как в Python
title = "My App"
debug = false
port = 8080
hosts = ["alpha.example.com", "beta.example.com"]
released = 2026-05-13

[database]
host = "localhost"
port = 5432
user = "postgres"

[airflow.dag]
schedule = "0 3 * * *"
catchup = false
tags = ["etl", "daily"]

Что здесь нужно знать.

Ключ = значение на одной строке. Без двоеточий, без отступов.

Типы пишутся как в Python/JSON. Строки в "...", числа без кавычек, булевы true/false строчными, массивы в [...], даты в формате ISO 8601 (2026-05-13, 2026-05-13T15:30:00Z).

Секции объявляются через [name]. Всё, что идёт ниже до следующей секции, попадает в этот объект. [airflow.dag] создаёт вложенный объект airflow.dag, то есть airflow = { dag = { ... } }. Это удобно для группировки настроек.

Комментарии#, как в Python. В JSON их нет — это одно из главных преимуществ TOML для конфигов.

Чтение в Python 3.11+ — через stdlib:

import tomllib
from pathlib import Path

with Path("config.toml").open("rb") as f:    # обязательно "rb" — бинарный режим
    config = tomllib.load(f)

# config — обычный dict
print(config["title"])
print(config["database"]["host"])
print(config["airflow"]["dag"]["schedule"])
WARNING

tomllib.load принимает файл, открытый в бинарном режиме ("rb"). Это не опечатка и не дизайн-промах — спецификация TOML формально требует UTF-8 байтов, а текстовый режим в Python потенциально разворачивает кодировку. Если откроете "r", получите TypeError.

Запись TOML stdlib не умеет — для этого ставится tomli-w:

uv add tomli-w
import tomli_w
from pathlib import Path

config = {
    "title": "My App",
    "database": {"host": "localhost", "port": 5432},
}

Path("config.toml").write_bytes(tomli_w.dumps(config).encode("utf-8"))

В практике DE запись TOML встречается редко — обычно конфиги пишут люди, читают программы. Зато чтение нужно везде: pyproject.toml, [tool.ruff], [tool.pytest], ваши собственные app.toml.

pyproject.toml как образец

Прямо сейчас в любом современном Python-проекте есть pyproject.toml — единый файл, описывающий проект. Вспомним его структуру (мы видели её в Module 01):

[project]
name = "my-etl"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.5",
    "structlog>=24.0",
]

[tool.ruff]
line-length = 100
target-version = "py313"

[tool.pytest.ini_options]
testpaths = ["tests"]

Это TOML «as is» — никаких надстроек, никаких комментариев. Программа, которая хочет узнать, какие у вас зависимости, делает буквально:

import tomllib
from pathlib import Path

config = tomllib.loads(Path("pyproject.toml").read_text())
for dep in config["project"]["dependencies"]:
    print(dep)

Парсинг — две строки, никаких сторонних библиотек. Это сила TOML.

YAML: мощный и опасный

YAML расшифровывается как YAML Ain’t Markup Language (рекурсивная аббревиатура). Создавался как «человеческий» формат для конфигов с упором на читаемость через отступы. По факту — стал стандартом везде, где конфиги пишут DevOps: Docker Compose, Kubernetes, Airflow, GitLab CI, Ansible, Helm.

# Тот же самый конфиг, что был TOML-ом
title: My App
debug: false
port: 8080
hosts:
  - alpha.example.com
  - beta.example.com
released: 2026-05-13

database:
  host: localhost
  port: 5432
  user: postgres

airflow:
  dag:
    schedule: "0 3 * * *"
    catchup: false
    tags:
      - etl
      - daily

Главные синтаксические правила.

Вложенность через отступы. Сколько пробелов — не важно, важна консистентность внутри одного блока. Обычно 2 пробела. Табуляция запрещена. Это типичный источник YAML-багов: «то ли два, то ли четыре, то ли таб».

Строки могут быть без кавычек. port: 8080 — это число, host: localhost — это строка. Парсер решает по типу значения. Кавычки нужны только когда хочется заставить YAML считать значение строкой («yes», «no», «3.14» как строки) или когда в значении специальные символы.

Массивы через дефис на отдельных строках или [a, b, c] inline.

Ссылки и якоря&name/*name, позволяют переиспользовать блоки. Лучше про них знать, но самим использовать в начале не стоит — это источник запутанности.

Стандартный парсер для Python — PyYAML:

uv add pyyaml
import yaml
from pathlib import Path

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

print(config["database"]["host"])

Самое важное про PyYAML: всегда safe_load

В PyYAML есть две функции: yaml.load и yaml.safe_load. Никогда не используйте yaml.load. Это правило не обсуждается.

Дело в том, что полноценный YAML поддерживает теги вроде !!python/object, которые могут сериализовать любой Python-объект, включая код. Парсер yaml.load выполняет такие теги — и злонамеренный YAML может выполнить произвольный код на вашей машине. Это была известная CVE-2017-18342: даже в документации PyYAML теперь жирно предупреждение.

yaml.safe_load — урезанная версия, которая поддерживает только базовые YAML-типы (string, number, bool, null, list, dict). Этого достаточно для всех нормальных конфигов и безопасно для непроверенных файлов.

# ВСЕГДА:
config = yaml.safe_load(f)

# НИКОГДА:
config = yaml.load(f)              # уязвимость
config = yaml.load(f, Loader=yaml.Loader)  # та же уязвимость

# Корректно с явным loader:
config = yaml.load(f, Loader=yaml.SafeLoader)  # эквивалентно safe_load

То же для записи: используйте yaml.safe_dump, не yaml.dump.

YAML quirks: Norway problem

YAML — это формат с самым высоким количеством сюрпризов на квадратный сантиметр спецификации. Junior должен знать минимум три типичных подвоха.

1. Norway problem. Это самый известный кейс. Если у вас YAML с кодами стран:

countries:
  - DE
  - FR
  - NO    # ← Norway
  - RU

В YAML 1.1 (которую по умолчанию использует PyYAML до недавнего времени) строка NO без кавычек парсится как булево false. То же самое с YES, ON, OFF, TRUE, FALSE в любых регистрах. И вот вы вместо «Норвегии» получаете False в списке.

Решение: либо кавычки "NO", либо обновить PyYAML и указать version="1.2", либо использовать парсер ruamel.yaml, который ведёт себя честнее.

2. Octal numbers. В YAML 1.1 значение 010 — это восемь (восьмеричное), а не десять. Любые «лидирующие нули» в числах опасны. Если значение — ID или версия с лидирующим нулём (030), обязательно кавычки: "030".

3. Версии (1.1 vs 1.2). Парсеры разных приложений могут использовать разные версии спецификации. PyYAML — 1.1 по умолчанию (в новых версиях улучшается). Это означает, что один и тот же YAML может по-разному парситься в Python и в Kubernetes. Если что-то странное — проверьте версию.

TIP

Кратчайшее правило ручной YAML-гигиены: строки, которые могут парситься как булево или число, оборачивайте в кавычки. "yes", "no", "030", "true" как строки — кавычки. Это занимает на два символа больше, но защищает от Norway-problem и подобного.

JSON vs YAML vs TOML: когда что

Резюме:

JSONYAMLTOML
Жанрданныеконфигконфиг
Комментариинетда (#)да (#)
Поддержка datetimeнетдада
Вложенность{...}отступысекции [a.b]
Сюрпризымаломного (Norway, octal)мало
Stdlib чтениеданет (PyYAML)да (3.11+)
Где встречаетсяAPI, логиDocker, K8s, Airflowpyproject, Rust, новые tools

Решение junior’а:

  • Конфиг своего Python-приложения → TOML. stdlib, понятный синтаксис, никаких сюрпризов.
  • Конфиг Airflow DAG / Docker Compose / Kubernetes / CI-pipeline → YAML, потому что так требует инструмент.
  • Обмен данных между сервисами → JSON или JSONL (см. прошлый урок).

DE-кейс: конфиг Airflow DAG’а

Очень частый паттерн — конфиг DAG’а отдельно от кода. Это позволяет переключать ETL-параметры (расписание, окно дат, источники) без редактирования Python-файла.

dag_config.yaml:

dag:
  id: orders_etl
  schedule: "0 3 * * *"
  catchup: false
  tags:
    - etl
    - daily
  max_active_runs: 1

source:
  type: postgres
  conn_id: production_db
  query: |
    SELECT id, amount, created_at
    FROM orders
    WHERE created_at >= '{{ ds }}'

sink:
  type: s3
  bucket: data-lake
  prefix: "raw/orders"
  partition_by: "ds"

retries:
  count: 3
  delay_seconds: 60

И валидация при загрузке — через Pydantic-модель из Module 04:

from pathlib import Path
import yaml
from pydantic import BaseModel, Field


class SourceConfig(BaseModel):
    type: str
    conn_id: str
    query: str


class SinkConfig(BaseModel):
    type: str
    bucket: str
    prefix: str
    partition_by: str


class RetriesConfig(BaseModel):
    count: int = Field(ge=0, le=10)
    delay_seconds: int = Field(ge=0)


class DagConfig(BaseModel):
    id: str
    schedule: str
    catchup: bool
    tags: list[str]
    max_active_runs: int = Field(ge=1, le=10)


class ETLConfig(BaseModel):
    dag: DagConfig
    source: SourceConfig
    sink: SinkConfig
    retries: RetriesConfig


def load_config(path: Path) -> ETLConfig:
    raw = yaml.safe_load(path.read_text(encoding="utf-8"))
    return ETLConfig.model_validate(raw)

Что мы здесь получаем. YAML — для удобного редактирования людьми (DevOps, аналитики правят свои значения). Pydantic — для валидации: если count отрицательный, если schedule пропущен, если conn_id не строка — упадём на загрузке, а не глубоко в середине DAG’а.

Это базовый «defensive» паттерн: формат для людей + валидация для программы. Чем раньше упасть на плохом конфиге, тем меньше платить за инцидент.

DE-кейс: app.toml для своего приложения

Для собственных Python-приложений можно использовать TOML без сторонних библиотек:

app.toml:

[app]
name = "events-collector"
log_level = "INFO"

[api]
base_url = "https://api.example.com"
timeout_seconds = 30
retries = 3

[archive]
out_dir = "/var/lib/archive"
gzip_compresslevel = 6
import tomllib
from pathlib import Path
from pydantic import BaseModel, HttpUrl, Field


class APIConfig(BaseModel):
    base_url: HttpUrl
    timeout_seconds: int = Field(ge=1, le=300)
    retries: int = Field(ge=0, le=10)


class ArchiveConfig(BaseModel):
    out_dir: Path
    gzip_compresslevel: int = Field(ge=1, le=9)


class AppConfig(BaseModel):
    name: str
    log_level: str
    api: APIConfig
    archive: ArchiveConfig


def load_app_config(path: Path) -> AppConfig:
    raw = tomllib.loads(path.read_text(encoding="utf-8"))
    # «склеиваем» секции [app], [api], [archive] в плоский dict для Pydantic
    flat = {**raw["app"], "api": raw["api"], "archive": raw["archive"]}
    return AppConfig.model_validate(flat)

Никаких внешних зависимостей. tomllib — stdlib, Pydantic у вас уже стоит. Это и есть «легковесный конфиг» для большинства боевых сервисов: TOML для редактирования, Pydantic для валидации, никаких YAML-сюрпризов.

Что мы получили

  • TOML — современный конфиг-формат, в stdlib с Python 3.11 (tomllib). Чёткий, без сюрпризов, идеален для своих приложений.
  • TOML открывают в бинарном режиме ("rb"), для записи нужен tomli-w.
  • YAML — мейнстрим для DevOps-инструментов (Docker, K8s, Airflow, CI). Читать через yaml.safe_load, никогда через yaml.load.
  • YAML quirks: Norway problem (NOFalse), octal numbers (030 → 24), различия между версиями 1.1/1.2. Защита — кавычки вокруг подозрительных строк.
  • DE-паттерн: YAML/TOML + Pydantic-модель — формат для людей, валидация для программы. Падать на bad config рано, а не глубоко в pipeline.

В следующем уроке — Parquet через pyarrow: бинарный columnar формат, на котором живёт весь современный аналитический стек.

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5