Почему конфиги — отдельный жанр
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"])
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. Если что-то странное — проверьте версию.
Кратчайшее правило ручной YAML-гигиены: строки, которые могут парситься как булево или число, оборачивайте в кавычки. "yes", "no", "030", "true" как строки — кавычки. Это занимает на два символа больше, но защищает от Norway-problem и подобного.
JSON vs YAML vs TOML: когда что
Резюме:
| JSON | YAML | TOML | |
|---|---|---|---|
| Жанр | данные | конфиг | конфиг |
| Комментарии | нет | да (#) | да (#) |
| Поддержка datetime | нет | да | да |
| Вложенность | {...} | отступы | секции [a.b] |
| Сюрпризы | мало | много (Norway, octal) | мало |
| Stdlib чтение | да | нет (PyYAML) | да (3.11+) |
| Где встречается | API, логи | Docker, K8s, Airflow | pyproject, 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 (
NO→False), octal numbers (030→ 24), различия между версиями 1.1/1.2. Защита — кавычки вокруг подозрительных строк. - DE-паттерн: YAML/TOML + Pydantic-модель — формат для людей, валидация для программы. Падать на bad config рано, а не глубоко в pipeline.
В следующем уроке — Parquet через pyarrow: бинарный columnar формат, на котором живёт весь современный аналитический стек.