How do you update multiple properties on a pydantic model that are validated together and dependent upon each other?
Here is a contrived but simple example:
from pydantic import BaseModel, root_validator
class Example(BaseModel):
a: int
b: int
@root_validator
def test(cls, values):
if values['a'] != values['b']:
raise ValueError('a and b must be equal')
return values
class Config:
validate_assignment = True
example = Example(a=1, b=1)
example.a = 2 # <-- error raised here because a is 2 and b is still 1
example.b = 2 # <-- don't get a chance to do this
Error:
ValidationError: 1 validation error for Example
__root__
a and b must be equal (type=value_error)
Both a
and b
having a value of 2
is valid, but they can't be updated one at a time without triggering the validation error.
Is there a way to put the validation on hold until both are set? Or a way to somehow update both of them at the same time? Thanks!
I found a couple solutions that works well for my use case.
__dict__
of the pydantic instance directly if it passes -- see update
methoddelay_validation
methodfrom pydantic import BaseModel, root_validator
from contextlib import contextmanager
import copy
class Example(BaseModel):
a: int
b: int
@root_validator
def enforce_equal(cls, values):
if values['a'] != values['b']:
raise ValueError('a and b must be equal')
return values
class Config:
validate_assignment = True
def update(self, **kwargs):
self.__class__.validate(self.__dict__ | kwargs)
self.__dict__.update(kwargs)
@contextmanager
def delay_validation(self):
original_dict = copy.deepcopy(self.__dict__)
self.__config__.validate_assignment = False
try:
yield
finally:
self.__config__.validate_assignment = True
try:
self.__class__.validate(self.__dict__)
except:
self.__dict__.update(original_dict)
raise
example = Example(a=1, b=1)
# ================== This didn't work: ===================
# example.a = 2 # <-- error raised here because a is 2 and b is still 1
# example.b = 2 # <-- don't get a chance to do this
# ==================== update method: ====================
# No error raised
example.update(a=2, b=2)
# Error raised as expected - a and b must be equal
example.update(a=3, b=4)
# Error raised as expected - a and b must be equal
example.update(a=5)
# # =============== delay validation method: ===============
# No error raised
with example.delay_validation():
example.a = 2
example.b = 2
# Error raised as expected - a and b must be equal
with example.delay_validation():
example.a = 3
example.b = 4
# Error raised as expected - a and b must be equal
with example.delay_validation():
example.a = 5
You can make a workaround building a setter.
from pydantic import BaseModel, root_validator
class Example(BaseModel):
a: int
b: int
@root_validator
def test(cls, values):
if values['a'] != values['b']:
raise ValueError('a and b must be equal')
return values
class Config:
validate_assignment = True
def set_a_and_b(self, value):
self.Config.validate_assignment = False
self.a, self.b = value, value
self.Config.validate_assignment = True
PoC:
>>> example = Example(a=1, b=1)
>>> example.a = 2
Traceback (most recent call last):
File "D:\temp\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3398, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-4-950b5db07c46>", line 1, in <cell line: 1>
example.a =2
File "pydantic\main.py", line 393, in pydantic.main.BaseModel.__setattr__
pydantic.error_wrappers.ValidationError: 1 validation error for Example
__root__
a and b must be equal (type=value_error)
>>> example.set_a_and_b(2) # <========= workaround
>>> example
Example(a=2, b=2)
>>> example.a = 3
Traceback (most recent call last):
File "D:\temp\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3398, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-8-d93e8eb8a0e3>", line 1, in <cell line: 1>
example.a = 3
File "pydantic\main.py", line 393, in pydantic.main.BaseModel.__setattr__
pydantic.error_wrappers.ValidationError: 1 validation error for Example
__root__
a and b must be equal (type=value_error)
But maybe in your real case you should use some setters and getters instead (or with) standard validation
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With