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?

Asked By: Trojo

||

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
Answered By: Tanveer Ahmad

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
Answered By: Daniil Fajnberg
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.