aiven
aiven

Reputation: 4323

how to compare field value with previous one in pydantic validator?

What I tried so far:

from pydantic import BaseModel, validator


class Foo(BaseModel):
    a: int
    b: int
    c: int

    class Config:
        validate_assignment = True

    @validator("b", always=True)
    def validate_b(cls, v, values, field):
        # field - doesn't have current value
        # values - has values of other fields, but not for 'b'
        if values.get("b") == 0:  # imaginary logic with prev value
            return values.get("b") - 1
        return v


f = Foo(a=1, b=0, c=2)
f.b = 3
assert f.b == -1  # fails

Also looked up property setters but apparently they don't work with pydantic.

Looks like bug to me, so I made an issue on github: https://github.com/pydantic/pydantic/issues/4888

Upvotes: 1

Views: 2563

Answers (1)

Daniil Fajnberg
Daniil Fajnberg

Reputation: 18751

The way validation is intended to work is stateless. When you create a model instance, validation is run before the instance is even fully initialized.

You mentioned relevant sentence from the documentation about the values parameter:

values: a dict containing the name-to-value mapping of any previously-validated fields

If we ignore assignment for a moment, for your example validator, this means the values for fields that were already validated before b, will be present in that dictionary, which is only the value for a. (Because validators are run in the order fields are defined.) This description is evidently meant for the validators run during initialization, not assignment.

What I would concede is that the documentation is leaves way too much room for interpretation as to what should happen, when validating value assignment. If we take a look at the source code of BaseModel.__setattr__, we can see the intention very clearly though:

def __setattr__(self, name, value):
   ...
        known_field = self.__fields__.get(name, None)
        if known_field:
            # We want to
            # - make sure validators are called without the current value for this field inside `values`
            # - keep other values (e.g. submodels) untouched (using `BaseModel.dict()` will change them into dicts)
            # - keep the order of the fields
            if not known_field.field_info.allow_mutation:
                raise TypeError(f'"{known_field.name}" has allow_mutation set to False and cannot be assigned')
            dict_without_original_value = {k: v for k, v in self.__dict__.items() if k != name}
            value, error_ = known_field.validate(value, dict_without_original_value, loc=name, cls=self.__class__)
    ...

As you can see, it explicitly states in that comment that values should not contain the current value.

We can observe that this is actually the displayed behavior here:

from pydantic import BaseModel, validator


class Foo(BaseModel):
    a: int
    b: int
    c: int

    class Config:
        validate_assignment = True

    @validator("b")
    def validate_b(cls, v: object, values: dict[str, object]) -> object:
        print(f"{v=}, {values=}")
        return v


if __name__ == "__main__":
    print("initializing...")
    f = Foo(a=1, b=0, c=2)
    print("assigning...")
    f.b = 3

Output:

initializing...
v=0, values={'a': 1}
assigning...
v=3, values={'a': 1, 'c': 2}

Ergo, there is no bug here. This is the intended behavior.

Whether this behavior is justified or sensible may be debatable. If you want to debate this, you can open an issue as a question and ask why it was designed this way and argue for a plausible alternative approach.

Though in my personal opinion, what is more strange in the current implementation is that values contains anything at all during assignment. I would argue this is strange since only that one specific value being assigned is what is validated. The way I understand the intent behind values it should only be available during initialization. But that is yet another debate.

What is undoubtedly true is that this behavior of validator methods upon assignment should be explicitly documented. This is also something that may be worth mentioning in the aforementioned issue.

Upvotes: 3

Related Questions