Модель прошла проверку и испортилась: validate_assignment в Pydantic

Настройки загрузчика создаются с положительным batch_size. Входной JSON проверен, тест неверного нуля проходит. Затем служебная функция меняет settings.batch_size на ноль, и следующий запуск делит данные на неправильные порции. У модели есть аннотация и Field(gt=0), но создание объекта и последующее присваивание — разные пути изменения состояния.

Проверка при записи атрибута

По умолчанию обычная Pydantic-модель проверяет вход при создании. validate_assignment=True включает проверку присваивания полям уже существующего экземпляра. Это полезно, когда объект служит изменяемой конфигурацией или промежуточной командой и разные функции могут менять его значения. Если таких изменений нет, проще вообще создавать новый проверенный объект при обновлении.

Начнём с одного поля: положительного размера порции. Затем добавим список идентификаторов, чтобы увидеть важное различие. Присваивание нового списка проходит через модель, а вызов append меняет внутренний объект списка. Он не является записью атрибута settings.ids и не запускает ту же проверку. Поэтому изменяемые вложенные значения требуют отдельного выбора структуры.

Три способа изменения в одном тесте

Установите Pydantic v2 и запустите блок Python. Пример проверяет успешное изменение, отказ при нуле и сохранность прежнего значения. Отдельно показана мутация списка: без этого тест мог бы создать впечатление, что параметр автоматически защищает всё вложенное состояние.

from pydantic import BaseModel, ConfigDict, Field, ValidationError

class Settings(BaseModel):
    model_config = ConfigDict(validate_assignment=True)
    batch_size: int = Field(gt=0)
    ids: list[int]

settings = Settings(batch_size=10, ids=[1])
settings.batch_size = 25
assert settings.batch_size == 25
try:
    settings.batch_size = 0
except ValidationError as exc:
    assert exc.errors()[0]["loc"] == ("batch_size",)
else:
    raise AssertionError("zero batch accepted")
assert settings.batch_size == 25

try:
    settings.ids = ["invalid"]
except ValidationError:
    pass
else:
    raise AssertionError("invalid replacement accepted")
assert settings.ids == [1]
settings.ids.append("invalid")
assert settings.ids == [1, "invalid"]
print({"batch_size": settings.batch_size, "mutated_ids": settings.ids})

Что выбрать для вложенных значений

Если набор идентификаторов должен заменяться целиком, используйте неизменяемый tuple и присваивайте новый проверенный набор. Для большого изменяемого состояния полезно вынести операции в методы предметной модели и проверять там правило изменения. Сам список не знает об ограничениях Pydantic, поэтому запрет неверного append должен находиться там, где append действительно выполняется.

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

Для правил сразу нескольких полей удобнее собрать новый набор значений и валидировать его целиком. Например, изменение начала и конца интервала по одному атрибуту способно временно нарушить условие start < end, хотя окончательная пара допустима. Операция замены интервала должна принимать оба значения и создавать согласованное состояние одним шагом.

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

В статье о validate_default разбирается другой вход — значение из объявления поля. Здесь значение уже однажды проверено и меняется во время работы программы.

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

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

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

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

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