Поля конфигурации перепутались местами: keyword-only dataclass

Настройки подключения начинаются с двух строк: адреса основного узла и адреса резервного. Позже добавляются окружение и имя приложения. Вызов Settings("primary", "backup", "prod", "billing") выглядит компактно, но на ревью приходится вспоминать порядок полей. Перестановка двух строк не вызывает исключения и меняет поведение программы.

Для объектов конфигурации имена аргументов часто важнее краткости. Параметр kw_only=True в dataclass заставляет передавать поля по имени. Это правило действует в сгенерированном конструкторе: позиционный вызов заканчивается TypeError. На Python 3.10 и новее такая возможность есть в стандартной библиотеке.

Имена вместо порядка полей

Сделаем объект, где явно видны основная и резервная точки. Заодно сохраним валидацию в __post_init__, чтобы пустой основной адрес не прошёл при обычном создании. Аннотация str сама по себе не проверяет внешние данные. Здесь нас интересуют две разные ошибки: неверная форма вызова и недопустимое значение обязательной настройки.

from dataclasses import dataclass

@dataclass(kw_only=True)
class Settings:
    primary: str
    fallback: str
    environment: str = "dev"

    def __post_init__(self) -> None:
        if not self.primary:
            raise ValueError("primary is required")

settings = Settings(primary="primary", fallback="backup")
assert settings.environment == "dev"
print(settings.primary)

Переход существующих вызовов

Сохраните файл и запустите Python 3.12. Корректный объект напечатает primary. Для проверки защиты от старого вызова отдельно выполните Settings("primary", "backup"): появится TypeError. А Settings(primary="", fallback="backup") дойдёт до __post_init__ и получит ValueError. Эти проверки стоит держать раздельными, чтобы случайная ошибка конструктора не маскировала исчезнувшую валидацию.

При миграции большого проекта сначала найдите все вызовы Settings. Если конструктор используется в нескольких пакетах, переход меняет публичный API и требует обновить потребителей. Для постепенного перехода можно сделать именованной только новую группу полей через dataclasses.KW_ONLY. Это позволяет оставить короткий стабильный позиционный идентификатор и потребовать имена для остальных настроек. Выбор зависит от того, какие вызовы действительно удобно читать.

Не превращайте большой dataclass в универсальную корзину. Если половина полей нужна только одному компоненту, выделите отдельный объект и передавайте его этому компоненту. Имена аргументов устраняют неоднозначность вызова, но не исправляют смешение независимых конфигураций. Маленькие объекты также проще собирать в тестах: видно, какое значение влияет на проверяемый сценарий.

Хорошая дополнительная проверка — добавить новое поле с разумным значением по умолчанию и убедиться, что существующие именованные вызовы не изменились. Затем проверьте сериализацию, если объект сохраняется: требование keyword-only относится к конструктору, а формат хранения нужно поддерживать отдельно. После этого пример создания настройки становится её короткой документацией: автор вызова пишет, какое значение чему соответствует.

Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.

Посмотреть программу и практику курса.

Документация: официальный источник по теме.

Другие задачи по теме: Типы, тесты и поведение программы.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.