Модель прошла проверку и испортилась: 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 к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
