Вложенная модель испорчена, но принята снова: revalidate_instances

Сервис принял проверенный объект адресата, добавил его в кэш и позже использовал при создании задания. Между этими шагами другой код изменил список каналов уведомления на неверное значение. Новая внешняя модель успешно создаётся, потому что получает уже готовый экземпляр нужного класса. Предположение «новый контейнер всё перепроверит» оказывается неверным.

Проверка относится к классу вложенного объекта

revalidate_instances определяет, нужно ли повторно проверять экземпляры модели, поступающие на валидацию. Значение always полезно на передаче изменяемого объекта в следующий сценарий. Настройка ставится на модель адресата, состояние которой нужно проверить. Одна настройка внешнего Job не заставит автоматически изменить политику каждого независимого вложенного класса.

Повторная проверка особенно важна, когда изменение происходит внутри списка: validate_assignment не перехватывает append. Впрочем, это не единственное решение. Можно запретить изменение структуры, хранить неизменяемое представление или создавать новые объекты при обновлении. Выбор зависит от того, нужен ли приложению вообще разделяемый изменяемый экземпляр между несколькими заданиями.

Сравним две политики на одном сценарии

Нужен Pydantic v2. Сначала воспроизводится принятие испорченного экземпляра при обычных настройках. Затем такой же объект с always отклоняется с путём ошибки до конкретного элемента. Наконец, допустимый адресат успешно попадает в задание.

from pydantic import BaseModel, ConfigDict, ValidationError

class UncheckedRecipient(BaseModel):
    channels: list[int]

class UncheckedJob(BaseModel):
    recipient: UncheckedRecipient

old = UncheckedRecipient(channels=[1])
old.channels.append("broken")
assert UncheckedJob(recipient=old).recipient.channels == [1, "broken"]

class Recipient(BaseModel):
    model_config = ConfigDict(revalidate_instances="always")
    channels: list[int]

class Job(BaseModel):
    recipient: Recipient

bad = Recipient(channels=[1])
bad.channels.append("broken")
try:
    Job(recipient=bad)
except ValidationError as exc:
    assert exc.errors()[0]["loc"] == ("recipient", "channels", 1)
    print({"rejected_at": exc.errors()[0]["loc"]})
else:
    raise AssertionError("mutated instance accepted")

good = Job(recipient=Recipient(channels=[1, 2]))
assert good.recipient.channels == [1, 2]
print(good.model_dump())

Как встроить проверку в обработку задания

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

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

Если важна идентичность экземпляра, проверьте её отдельно. После повторной валидации приложение не должно полагаться на то, что вернётся именно прежний Python-объект. Бизнес-смысл адресата определяется идентификатором и данными, а не выражением is. Это особенно важно для кэша, где объект ранее мог использоваться как общий контейнер состояния.

В тесте должны участвовать уже созданные модели. Передача только словарей всегда проверит входные значения и не воспроизведёт проблему. Добавьте последовательность создать → изменить вложенное значение → вложить в новую модель. Именно она показывает отличие этой настройки от обычного поля list[int].

Похожая тема проверки значений по умолчанию относится к созданию нового состояния из объявления. Здесь повторно используется существующий экземпляр с потенциально изменившимися данными.

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

Отделите сборку задания от хранения адресатов и покройте передачу объекта регрессионным тестом. В продвинутом Python есть композиция, типизация и тестируемый дизайн, которые помогут сократить скрытые изменения общего состояния. Откройте программу продвинутого Python.

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

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

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