Reputation: 4323
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
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