Короткий ответ
Немедленно сделать поле обязательным в коде и в сериализованном формате — это breaking change для всех, кто создаёт объект без нового поля или десериализует старые сообщения без него; безопасная эволюция обычно идёт через добровольный (optional) этап: поле добавляется с безопасным значением по умолчанию или как Optional, старые данные продолжают десериализоваться, а переход к реально обязательному полю выполняется отдельным скоординированным этапом с поддержкой потребителей и периодом депрекации.
Как это работает подробнее
Если поле добавлено как field: NewType без значения по умолчанию в существующий @dataclass, любой код, создающий объект позиционными/именованными аргументами без этого поля, немедленно перестанет работать (TypeError: missing required argument), а код, десериализующий старые JSON-сообщения без этого поля в словарь и передающий его как data в конструктор, упадёт точно так же. Добавление поля со значением по умолчанию (field: NewType = default или Optional[NewType] = None) сохраняет обратную совместимость конструктора и десериализации, позволяя постепенно обновлять потребителей, которые должны начать заполнять это поле осмысленным значением.
Пример кода
from dataclasses import dataclass
from typing import Optional
# Шаг 1: безопасное добавление поля, старые вызовы и JSON без поля продолжают работать
@dataclass
class OrderEvent:
order_id: str
amount: float
currency: str = "RUB" # уже существующее поле с default
risk_score: Optional[float] = None # новое поле — Optional на переходный период
# Старый вызывающий код и старые сообщения без risk_score не ломаются:
old_style = OrderEvent(order_id="o-1", amount=100.0)
print(old_style.risk_score) # None — потребитель должен уметь обработать этот случайЧто использовать на практике
- Практика. Типичная последовательность: (1) добавить поле как Optional с безопасным default и задеплоить библиотеку — старые потребители не ломаются; (2) обновить и задеплоить сервисы-производители данных, чтобы они начали заполнять новое поле; (3) добавить мониторинг/алерты на случаи, когда поле всё ещё приходит пустым от каких-то потребителей; (4) только после того, как все знаемые продюсеры подтверждённо заполняют поле, рассмотреть перевод его в по-настоящему обязательное в следующей major-версии библиотеки, задокументированной как breaking change с периодом депрекации. Для JSON-сериализации дополнительно стоит явно решить, как десериализатор обрабатывает отсутствие поля в входящих данных старых версий — это отдельная ось совместимости от конструктора класса на Python-уровне.
Подводные камни
- Риски. Добавляют обязательное поле и тестируют только "счастливый путь" с новым кодом на обоих концах, не проверяя обратную совместимость при встрече новой библиотеки со старыми сообщениями/вызовами, которые неизбежно существуют в проде при постепенном деплое. Также путают совместимость конструктора Python-объекта с совместимостью формата сериализации — поле может быть безопасным при создании объекта (есть default), но его отсутствие в JSON может по-прежнему вызывать ошибку в кастомной логике десериализации, если она не обработана отдельно.
- Компромисс. Поэтапный подход с default/Optional безопаснее, но требует больше календарного времени, координации между командами-потребителями и временного существования "неполных" данных без нового поля, которые нужно явно обрабатывать в бизнес-логике (что делать, если поле всё ещё отсутствует). Резкое немедленное изменение быстрее для автора библиотеки, но переносит всю стоимость и риск поломки на множество внешних команд одновременно, что при десятках потребителей обычно неприемлемо по операционным рискам.