Is there a way to execute a function after a Pydantic model is initialized?
Question:
Im trying to always provide the hashed id:
class MySchema(BaseModel):
id: int
hashed_id: str = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.hash_id()
def hash_id(self):
self.hashed_id = encode(self.id)
It doesn’t seem to do anything really.
It outputs:
{
"id": 10,
"hashed_id": null
}
But what I want it to output is this:
{
"id": 10,
"hashed_id": "SkaHGA" // some hash
}
How would I achieve this?
Answers:
class MySchema(BaseModel):
id: int
hashed_id: str = None
@validator('hashed_id')
def validate_hashed_id(cls, hashed_id: str):
# remaining stuff whatever you want
If you really want hashed_id
to be a field on your model, then a custom validator is what you want. You can configure it to trigger, even if you do not provide an explicit hashed_id
value during initialization using the always=True
argument.
A validator method can optionally accept an argument (typically named values
), which will be a dictionary of previously validated field values (mapped to the field names as keys). Since validation is done in the order that fields are defined and in your case hashed_id
comes after id
, the id
field value will already be present in that dictionary by the time the validation method is called.
I would also recommend setting the default of hashed_id
to the empty string rather than None
for the sake of type consistency, but that is a minor point.
Here is how you could do it:
from typing import Any
from pydantic import BaseModel, validator
def mock_encode(i: int) -> str:
return str(i + 1)
class MySchema(BaseModel):
id: int
hashed_id: str = ""
@validator("hashed_id", always=True)
def encode_id(cls, v: str, values: dict[str, Any]) -> str:
assert "id" in values, "sanity check"
if not v:
return mock_encode(values["id"])
return v # if provided explicitly, values is taken as is
obj = MySchema(id=123)
print(obj) # id=123 hashed_id='124'
Alternatively, if you don’t actually need hashed_id
as a normal field, you can simply define it as a private attribute and set it like you did before, then have a getter property to expose the value publicly:
from typing import Any
from pydantic import BaseModel, PrivateAttr
# ... mock_encode from before
class AlternativeSchema(BaseModel):
id: int
_hashed_id: str = PrivateAttr()
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self._hashed_id = mock_encode(self.id)
@property
def hashed_id(self) -> str:
return self._hashed_id
obj = AlternativeSchema(id=123)
print(obj) # id=123
print(obj.hashed_id) # 124
Im trying to always provide the hashed id:
class MySchema(BaseModel):
id: int
hashed_id: str = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.hash_id()
def hash_id(self):
self.hashed_id = encode(self.id)
It doesn’t seem to do anything really.
It outputs:
{
"id": 10,
"hashed_id": null
}
But what I want it to output is this:
{
"id": 10,
"hashed_id": "SkaHGA" // some hash
}
How would I achieve this?
class MySchema(BaseModel):
id: int
hashed_id: str = None
@validator('hashed_id')
def validate_hashed_id(cls, hashed_id: str):
# remaining stuff whatever you want
If you really want hashed_id
to be a field on your model, then a custom validator is what you want. You can configure it to trigger, even if you do not provide an explicit hashed_id
value during initialization using the always=True
argument.
A validator method can optionally accept an argument (typically named values
), which will be a dictionary of previously validated field values (mapped to the field names as keys). Since validation is done in the order that fields are defined and in your case hashed_id
comes after id
, the id
field value will already be present in that dictionary by the time the validation method is called.
I would also recommend setting the default of hashed_id
to the empty string rather than None
for the sake of type consistency, but that is a minor point.
Here is how you could do it:
from typing import Any
from pydantic import BaseModel, validator
def mock_encode(i: int) -> str:
return str(i + 1)
class MySchema(BaseModel):
id: int
hashed_id: str = ""
@validator("hashed_id", always=True)
def encode_id(cls, v: str, values: dict[str, Any]) -> str:
assert "id" in values, "sanity check"
if not v:
return mock_encode(values["id"])
return v # if provided explicitly, values is taken as is
obj = MySchema(id=123)
print(obj) # id=123 hashed_id='124'
Alternatively, if you don’t actually need hashed_id
as a normal field, you can simply define it as a private attribute and set it like you did before, then have a getter property to expose the value publicly:
from typing import Any
from pydantic import BaseModel, PrivateAttr
# ... mock_encode from before
class AlternativeSchema(BaseModel):
id: int
_hashed_id: str = PrivateAttr()
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self._hashed_id = mock_encode(self.id)
@property
def hashed_id(self) -> str:
return self._hashed_id
obj = AlternativeSchema(id=123)
print(obj) # id=123
print(obj.hashed_id) # 124