Как запретить удаление полей в Pydantic BaseModel
При работе с Pydantic часто требуется защитить структуру модели от изменений. Стандартный BaseModel позволяет добавлять новые поля (если не настроена строгая валидация) и, что более критично, удалять существующие атрибуты через del obj.field. Это может привести к нарушению целостности данных и неожиданным ошибкам в рантайме. В этой статье мы разберём, как настроить модель так, чтобы удаление полей было запрещено.
Проблема удаления атрибутов в Pydantic
По умолчанию Pydantic BaseModel не блокирует операцию del. После удаления поля при обращении к нему вы получите AttributeError, что нежелательно для критичных данных. Пример:
from pydantic import BaseModel
class Foo(BaseModel):
name: str
obj = Foo(name='test')
del obj.name
print(obj.name) # AttributeErrorЧтобы этого избежать, необходимо переопределить магический метод __delattr__ в классе-наследнике.
Метод 1: Переопределение __delattr__
Самый простой способ - запретить удаление любых полей, переопределив __delattr__ и выбрасывая исключение AttributeError с понятным сообщением.
from pydantic import BaseModel
class ProtectedModel(BaseModel):
def __delattr__(self, name):
raise AttributeError(f'Удаление поля "{name}" запрещено')
class Foo(ProtectedModel):
name: str
obj = Foo(name='test')
del obj.name # AttributeError: Удаление поля "name" запрещеноТакой подход полностью блокирует удаление атрибутов, включая служебные поля, начинающиеся с подчёркивания. Если нужно разрешить удаление некоторых полей, можно добавить проверку по имени.
Метод 2: Использование конфигурации модели
Pydantic v2 предоставляет возможность настроить модель как неизменяемую через параметр frozen=True в конфигурации. Это запрещает не только удаление, но и изменение значений полей после создания.
from pydantic import BaseModel, ConfigDict
class Foo(BaseModel):
model_config = ConfigDict(frozen=True)
name: str
obj = Foo(name='test')
obj.name = 'new' # ValidationError: Instance is frozen
del obj.name # ValidationError: Instance is frozenОбратите внимание: при frozen=True вы не сможете изменять поля вообще. Если требуется только запретить удаление, но разрешить изменение значений, лучше использовать первый метод.
Метод 3: Кастомный дескриптор для поля
Для более тонкого контроля можно создать дескриптор, который блокирует удаление конкретного поля, но оставляет возможность удалять другие. Это полезно, если только часть полей должна быть защищена.
from pydantic import BaseModel
class NonDeletable:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __delete__(self, obj):
raise AttributeError(f'Поле "{self.name}" нельзя удалить')
class Foo(BaseModel):
name: str = NonDeletable()
age: int
obj = Foo(name='test', age=25)
del obj.name # AttributeError
del obj.age # OKЭтот метод сложнее, но даёт максимальную гибкость, особенно если в модели есть поля, которые можно удалять.
Сравнение подходов
- __delattr__ - простой и универсальный, блокирует удаление всех атрибутов.
- frozen=True - полная неизменяемость, подходит для value-объектов.
- Дескриптор - точечный контроль, но требует больше кода.
Выбор зависит от задачи: если нужно защитить все поля модели от удаления, достаточно переопределить __delattr__. Если требуется полная неизменяемость - используйте frozen=True. Для частичной защиты применяйте дескрипторы.
Заключение
Защита структуры Pydantic от удаления полей - важная задача для обеспечения целостности данных. Встроенные средства не предоставляют такого механизма по умолчанию, но его легко реализовать через переопределение магических методов или конфигурацию модели. Рекомендуется комбинировать эти подходы с валидацией для достижения максимальной надёжности приложения.