Updating multiple Pydantic fields that are validated together

Question:

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!

Asked By: Zachary Duvall

||

Answers:

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:tempvenvlibsite-packagesIPythoncoreinteractiveshell.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 "pydanticmain.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:tempvenvlibsite-packagesIPythoncoreinteractiveshell.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 "pydanticmain.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

Answered By: JacekK

I found a couple solutions that works well for my use case.

  1. manually triggering the validation and then updating the __dict__ of the pydantic instance directly if it passes — see update method
  2. a context manager that delays validation until after the context exits — see delay_validation method
from 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
Answered By: Zachary Duvall