Pydantic: How to pass the default value to a variable if None was passed?

Question:

Can I make a default value in Pydantic if None is passed in the field?

I have the following code, but it seems to me that the validator here only works on initialization of the model and not otherwise.

My Code:

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

    @validator('name')
    def set_name(cls, name):
        return name or 'foo'

Problem Encountered:

user = User(name=None, password='some_password', email='[email protected]')
print("Name is ", user.name)
# > 'Name is foo'

user.name = None
print("Name is ", user.name)
# > 'Name is None'

Desired Output:

user = User(name='some_name', password='some_password', email='[email protected]')
user.name = None
print("Name is ", user.name)
# > 'Name is foo'

Any ideas on how I can obtain the desired output? I think having getters and setters will help in tackling the issue. However, I could not get them to work in a Pydantic model:

Attempting to implement getters and setters:

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

    def get_password(self):
        return self.password

    def set_password(self, password):
        self.password = hash_password(password)

    password = property(get_password, set_password)

user = User(name='some_name', password='some_password', email='[email protected]')
# > RecursionError: maximum recursion depth exceeded

I also tried the property decorator:

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

    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, password):
        pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
        self._password = pwd_context.hash(password)

user = User(name='some_name', email='[email protected]')
user.password = 'some_password'
# > ValueError: "User" object has no field "password"

I also tried overwriting the init:

class User(BaseModel):
name: Optional[str] = ""
password: Optional[str] = ""
email: EmailStr

def __init__(self, name, password, email):
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
    password = pwd_context.hash(password)
    super().__init__(name=name, password=password, email=email)


user = User(name="some_name", password="some_password", email='[email protected]')
print(user.password)
# > AYylwSnbQgCHrl4uue6kO7yiuT20lazSzK7x # Works as expected

user.password = "some_other_password"
print(user.password)
# > "some_other_password" # Does not work

user.password = None
print(user.password)
# > None # Does not work either
Asked By: Manas Sambare

||

Answers:

You need to enable validate_assignment option in model config:

from typing import Optional

from pydantic import BaseModel, validator


class User(BaseModel):
    name: Optional[str] = ''
    password: Optional[str] = ''

    class Config:
        validate_assignment = True

    @validator('name')
    def set_name(cls, name):
        return name or 'foo'


user = User(name=None, password='some_password', )
print("Name is ", user.name)


user.name = None
print("Name is ", user.name)
Name is  foo
Name is  foo
Answered By: alex_noname

This question asked perfectly so i wanted to provide a wider example, because there are many ways to assign a value dynamically.

Alex’s answer is correct but it only works on when the Field directly inherits a dataclass more specifically something like this won’t work.

class User(BaseModel):
    name: Optional[str] = ""
    password: Optional[str] = ""

    class Config:
        validate_assignment = True

    @validator("name")
    def set_name(cls, name):
        return name or "bar"


user_dict = {"password": "so_secret"}
user_one = User(**user_dict)
Out: name='' password='so_secret'

Validate Always

For performance reasons, by default validators are not called for fields when a value is not supplied. But situations like this when you need to set a Dynamic Default Value we can set that to True

class User(BaseModel):
    name: Optional[str] = ""

    @validator("name", pre=True, always=True)
    def set_name(cls, name):
        return name or "bar"

In: user_one = User(name=None)
In: user_two = User()
Out: name='bar'
Out: name='bar'

But there is a one important catch with always, since we are using always=True pydantic would try to validate the default None which would cause an error.

Setting Pre to True it will call that field before validation error occurs, the default of a validator pre is set to False , in which case they’re called after field validation.

Using Config

But this has some disadvantages.

class User(BaseModel):
    name: Optional[str] = ""

    class Config:
        validate_assignment = True

    @validator("name")
    def set_name(cls, name):
        return name or "foo"

In:  user = User(name=None)
Out: name='foo'

When you set it to None it returns the dynamic value correctly but some situations like it is completely None, it fails.

In:  user = User()
Out: name=''

Again you need to set, to make that work.

pre=True
always=True

Using default_factory

This is mostly useful in cases when you want to set a default value, like UUID or datetime etc. In that cases you might want to use default_factory, but there is a big catch you can’t assign a Callable argument to the default_factory.

class User(BaseModel):
    created_at: datetime = Field(default_factory=datetime.now)

In: user = User()
Out: created_at=datetime.datetime(2020, 8, 29, 2, 40, 12, 780986)
Answered By: Yagiz Degirmenci

Many ways to assign a default value

Method #1: A required id field with default value

class User(BaseModel):
    id: str = uuid.uuid4()

Method #2 An optional id field with default value

class User(BaseModel):
    id: Optional[str] = uuid.uuid4()

Method #3: A required id field with default value

class User(BaseModel):
    id: str = Field(default=uuid.uuid4())

Method #4: A required id field with default value from callable. This is useful for generating on-demand values such as unique UUIDs or Timestamps.
See @yagiz-degirmenci answer.

class User(BaseModel):
    id: str = Field(default_factory=uuid.uuid4)  # uuid.uuid4 is not executed immediately
Answered By: dev-sareno

Adding an alternative option – convtools models (docs / github). This library doesn’t have implicit type casting, so when only types are defined, it runs validation only.

In your case – keep things separate:

  1. you define a field, its type and default value
  2. you post-process the resulting model by specifying a model-level validate class method, which returns (data, errors)
from typing import Optional

from convtools.contrib.models import DictModel, build


class User(DictModel):
    name: Optional[str] = ""

    @classmethod
    def validate(cls, data):
        data.name = data.name or ""
        return data, None


user, errors = build(User, {"name": None})
"""
>>> In [26]: build(User, {"name": None})
>>> Out[26]: (User(name=''), None)
>>> 
>>> In [27]: build(User, {"name": ""})
>>> Out[27]: (User(name=''), None)
>>> 
>>> In [28]: build(User, {"name": "123"})
>>> Out[28]: (User(name='123'), None)
>>> 
>>> In [29]: build(User, {"name": 123})
>>> Out[29]: (None, {'name': {'__ERRORS': {'type': 'int instead of NoneType/str'}}})
"""

Answered By: westandskif
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.