'Python [Pydantic] - default values based other object in the class

I would like to set default email in case email not provided, i.e:

name = a 
last_name = b
email = None 

email will become "[email protected]"

I tried something like that but obviously didn't work as the name,last_name not define in function.

class User(BaseModel):
     name: Optional[str] = ''
     last_name: Optional[str] = ''
     email: EmailStr
    

    @validator('email')
    def set_email(cls, email):
        if not email:
           return name + last_name + '@email.com' 
        else:
           return email

Update- still not working, i tried:

    @root_validator(pre=True)
    def email_set_config(cls, values):
        email, name ,last_name = values.get('email'), values.get('name') , values.get('last_name')
        if email is None :
            email= name + '_' + name + '@' + last_name 
        return values

Solution:

added to Class :

--Update

  • Actually it is working without define class Config:
    class Config:
        validate_assignment = True
    @validator('email')
    def set_email(cls, v, values, **kwargs):
        return email or values["name"] + '_' + values["name"] + '@' + ["last_name"]



Solution 1:[1]

Not enough reputation to comment, so I'll just expand on @NobbyNobbs answer here^^

For your last example using pydantic.validator, you can use the always kwargs to always run a validator even if the value is not provided.

Link in the doc: https://pydantic-docs.helpmanual.io/usage/validators/#validate-always

So your example would be:

from typing import Optional

import pydantic


class User(pydantic.BaseModel):
    first_name: str
    last_name: str
    email: Optional[pydantic.EmailStr]

    @pydantic.validator('email', always=True)
    def set_email(cls, v, values, **kwargs):    
        return v or values["first_name"] + '_' + values["last_name"] + '@email.com'


if __name__ == "__main__":
    print(User(first_name="jon", last_name="doe"))  # first_name='jon' last_name='doe' email='[email protected]'
    print(User(first_name="jon", last_name="doe", email=None))  # first_name='jon' last_name='doe' email='[email protected]'

Solution 2:[2]

I changed your model a bit, now it has two required fields and one optional, which calculated from the others two.

from typing import Optional

import pydantic


class User(pydantic.BaseModel):
    first_name: str
    last_name: str
    email: Optional[pydantic.EmailStr]

First approach to validate your data during instance creation, and have full model context at the same time, is using the @pydantic.root_validator:

    @pydantic.root_validator
    def email_set_config(cls, values):
        email, name, last_name = values.get('email'), values.get('first_name'), values.get('last_name')
        if email is None:
            values["email"] = name + '_' + last_name + '@email.com'
        return values


if __name__ == "__main__":
    u = User(first_name="jon", last_name="doe")
    print(u)  # first_name='jon' last_name='doe' email='[email protected]'

But if you want to control instantiation process only, I suggest you just override __init__ dunder in your model like this

    def __init__(__pydantic_self__, **data: Any) -> None:
        email, name, last_name = data.get('email'), data.get('first_name'), data.get('last_name')
        if email is None:
            data["email"] = name + '_' + last_name + '@email.com'
        super().__init__(**data)

It's more simple and intuitive approach comparing to validator on my sight, and I would prefer it.

The possible solution with non-root validator, you mention in question, has tricky and a bit unexpected behavior for me.

It works like expected if you pass named email argument to constructor, but doesn't if you not.

    @pydantic.validator('email')
    def set_email(cls, v, values, **kwargs):
        return v or values["first_name"] + '_' + values["last_name"] + '@email.com'


if __name__ == "__main__":
    print(User(first_name="jon", last_name="doe"))  # first_name='jon' last_name='doe' email=None
    print(User(first_name="jon", last_name="doe", email=None))  # first_name='jon' last_name='doe' email='[email protected]'

Solution 3:[3]

In my case the solution was:

@pydantic.root_validator(pre=True)
def method_before_validation(cls, data):

I used It in the root class of a complex object. I have smth like object with orders and separate object with client and I needed to put client's id to each order's object. So we can make data modification with data structure, types e.t.c on fly before the actual validation.

Solution 4:[4]

Pydantic has added default_factory since v1.5, you can use the below code to have the default EmailStr value, as well as the other submodel class object

class User(BaseModel):
     name: Optional[str] = ''
     last_name: Optional[str] = ''
     email: EmailStr = Field(
         default_factory=lambda: EmailStr('[email protected]'))

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Dharman
Solution 3 Yunnosch
Solution 4