pythondatabasepydanticdata-class

Pydanyic V2: how to write more intelligent `created_at`, `updated_at` fields


I wanna write a nice TimestampMixin class for my project, I hope it can:

  1. created_at: autonow when creation, and never change since that
  2. updated_at: autonow when creation
  3. updated_at: auto update updated_at if it is not specified when update.

like:

foo = Foo(name='foo') # created_at, updated_at autofilled

time.sleep(3)

foo.name = 'bar' # `updated_at` auto updated!

data_dict = {"name": "bar", "created_at": 1717288789, "updated_at": 1717288801}
foo_from_orm = Foo.model_validate(**data_dict) # `created_at`: 1717288789, `updated_at`: 1717288801 

For now, I have no solution, I can only manually write a on_update function and manually call it everytime when I update the model.

Is there any better solution or any ideas on this issue?

from datetime import datetime, UTC

from pydantic import BaseModel, Field


now_factory = lambda: int(datetime.now(UTC).timestamp())

class TimestampMixin(BaseModel):
    created_at: int = Field(default_factory=now_factory)
    updated_at: int = Field(default_factory=now_factory)

    def on_update(self):
        self.updated_at = now_factory()

Solution

  • Pydantic v2 answer:

    You can use validate_assignment from ConfigDict to force model validation after updating your model. Notice that validate_assignment is False by default.

    The only catch is that you need to turn off the validate_assignment before you update any field to prevent an infinite loop:

    @pydantic.model_validator(mode="after")
    @classmethod
    def set_updated_at(cls, obj):
        obj.model_config["validate_assignment"] = False
        obj.updated_at = now_factory()
        obj.model_config["validate_assignment"] = True
    
        return obj
    

    So in your case, here is a working example:

    now_factory = lambda: int(datetime.now(UTC).timestamp())
    
    
    class TimestampMixin(pydantic.BaseModel):
        model_config = pydantic.ConfigDict(
            validate_assignment=True,
        )
    
        created_at: int = pydantic.Field(
            default_factory=now_factory
        )
    
        updated_at: int | None = pydantic.Field(None)
    
        @pydantic.model_validator(mode="after")
        @classmethod
        def set_updated_at(cls, obj):
            obj.model_config["validate_assignment"] = False
            obj.updated_at = now_factory()
            obj.model_config["validate_assignment"] = True
    
            return obj
    
    
    class User(TimestampMixin, pydantic.BaseModel):
    
        name: str | None = pydantic.Field(None)
        age: int | None = pydantic.Field(None)
    
    
    if __name__ == "__main__":
        # create your object
        user = User()
        print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414213, 'name': None, 'age': None}
    
        time.sleep(2)
        user.name = "John"
        print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414215, 'name': 'John', 'age': None}
    
        time.sleep(2)
        user.age = 40
        print(user.model_dump()) # {'created_at': 1717414213, 'updated_at': 1717414217, 'name': 'John', 'age': 40}