Search code examples
pythonfastapipydanticinit

Need to provide addition steps to pydantic model initialisation method


I am trying to add custom steps to the Pydantic model __init__ method.

Pseudo Sample Code :

class Model(BaseModel):
    a: int
    b: Optional[List[int]] = None
    c: Optional[int] = None

    def __init__(self, *args, **kwargs):
        super.__init__(*args, **kwargs)
        self.method()

    def method(self):
        assert self.a is not None, "Value for a is not set"  # As a safety precaution let's say
        self.c = sum(self.b)  # Adds all the values in B and sets the sum to C

When I initialise the method

[In]: x = Model(a=1, b=[1, 2, 3, 4])
[Out]: pydantic.error_wrappers.ValidationError: 1 validation error for A
     response -> Model -> 0
     Value for a is not set (type=value_error)

I get this error even thou I have set both a and b in the model. Can someone help me out

The same problem works if the method is called outside the __init__ method, but I want the method to be called upon initialisation and not manually

Pseudo Sample Code :

class Model(BaseModel):
    a: int
    b: Optional[List[int]] = None
    c: Optional[int] = None

    def method(self):
        assert self.a is not None, "Value for a is not set"  # As a safety precaution let's say
        self.c = sum(self.b)  # Adds all the values in B and sets the sum to C

When I initialise the method

[In]: x = Model(a=1, b=[1, 2, 3, 4])
[Out]: Model(a=1, b=[1, 2 , 3 , 4], c=None)

[In]: x.method()
[Out]: Model(a=1, b=[1, 2 , 3 , 4], c=10)

Note: I'm restricted with the pydantic version '1.10.14'and cannot upgrade it for several reasons.


Update Jun 18th

After getting a few answers and help, I went with the validator method. But I went through few hiccups, which I have explained below

Updated pseudo code :

from pydantic import validator

class Model(BaseModel):
    a: int
    b: Optional[List[int]] = None
    c: Optional[int] = None

    @root_validator(pre=False, allow_reuse=False)
    def method(cls, values, **kwargs):

        assert values["b"] is not None, "Value for b is not set"
        values["c"] = sum(values["b"])

        return values

    def serialise(self):
        ...
        // Creates protobuf message

    @classmethod
    def deserialize(cls, message) -> "Model":
        ...
        // reads the protobuf message and creates the cls
        
        return cls(
            a = message.a
            c = message.c
        )

>>> x = Model(a=1, b=[1, 2, 3, 4]))
    
    Model(a=1 b=[1, 2, 3, 4] c=10)
>>>
>>> x.serialise()

    xxxxxxxxxxxxxxxxxxxxx
>>>
>>> y = Model.desirealise("xxxxxxxxxxxxxxxxxxxxx")

    Value for b is not set

In this case, I don't want to run the validator when c is manually set during the model is initiated.

Hence, I set skip_on_failure flag to True for the validator and I have made the below changes to prevent the check from failing.

from pydantic import validator

class Model(BaseModel):
    a: int
    b: Optional[List[int]] = None
    c: Optional[int] = None

    @root_validator(pre=False, allow_reuse=False)
    def method(cls, values, **kwargs):
        if values["c"] is not None:
            return values

        assert values["b"] is not None, "Value for b is not set"
        values["c"] sum(values["b"])

        return values

    def serialise(self):
        ...
        // Creates protobuf message

    @classmethod
    def deserialize(cls, message) -> "Model":
        ...
        // reads the protobuf message and creates the cls
        
        return cls(
            a = message.a
            c = message.c
        )

>>> x = Model(a=1, b=[1, 2, 3, 4]))
    
    Model(a=1 b=[1, 2, 3, 4] c=10)
>>>
>>> x.serialise()

    xxxxxxxxxxxxxxxxxxxxx
>>>
>>> y = Model.desirealise("xxxxxxxxxxxxxxxxxxxxx")

    Model(a=1, b=None, c=10)

This code resolves my issues and works properly with the system, but I want to know if this is a best practice or If any other way is there to do this.


Solution

  • Pydantic v2 answer:

    If you want to both validate and add logic to your property during initialization, you can use model_validator:

    from pydantic import BaseModel
    from pydantic import model_validator
    
    
    class Model(BaseModel):
        a: int
        b: Optional[List[int]] = None
        c: Optional[int] = None
    
    
        @model_validator(mode='after')
        def method(self):
            assert self.a is not None, "Value for a is not set"  # As a safety precausion let say
            self.c = sum(self.b)  # Adds all the value in B and sets the sum to C
    
    
    print(Model(a=1, b=[1, 2, 3, 4]))
    # a=1 b=[1, 2, 3, 4] c=10
    

    But I think it's a better approach to separate responsabilities with @model_validator and @computed_field:

    from pydantic import computed_field
    from pydantic import model_validator
    
    
    class Model(BaseModel):
        a: int
        b: Optional[List[int]] = None
    
        @model_validator(mode='after')
        def method(self):
            assert self.a is not None, "Value for a is not set"
    
        @computed_field
        @property
        def c(self) -> int:
            return sum(self.b)
    
    print(Model(a=1, b=[1, 2, 3, 4]))
    # a=1 b=[1, 2, 3, 4] c=10
    

    Pydantic v1 (1.10.14) answer:

    Since pydantic v1 don't offer computed_field, you can still rely on model/field validations. In this case, the validator decorator:

    from pydantic import validator
    
    
    class Model(BaseModel):
        a: int
        b: Optional[List[int]] = None
        c: Optional[int] = None
    
        @validator("c", always=True)
        def method(cls, v, values, **kwargs):
            assert values["a"] is not None, "Value for a is not set"
            return sum(values["b"])
    
    print(Model(a=1, b=[1, 2, 3, 4]))
    # a=1 b=[1, 2, 3, 4] c=10