Pydantic, allow property to be parsed or passed to constructor but make it immutable

Viewed 4433

I am trying to make a Pydantic model which for the most part is mutable BUT I want one member to be immutable.

Model

# Built In
from datetime import datetime

# 3rd Party
from pydantic import BaseModel
from pydantic import Field # https://pydantic-docs.helpmanual.io/usage/schema/#field-customisation

class Processor(BaseModel):
    """ Pydantic model for a Processor object within the database. """
    class Config:
        """
        Model Configuration: https://pydantic-docs.helpmanual.io/usage/model_config/
        """
        extra = 'forbid'
        allow_mutation = True # This allows mutation, cool
        validate_assignment = True
        underscore_attrs_are_private = True
    model: str
    created_at: str = Field(default_factory=datetime.utcnow().isoformat) # I want to make sure that this can be passed but not assignable
    updated_at: str
    version: int = 0
    expires: Optional[int] = None

My goal is to allow created_at to be parsed from a object (or defaulted) BUT prevent it from being assigned after the model is created.

Example

example_object = {
    "model": "foobar_model",
    "created_at": "2020-12-22T15:35:06.262454+00:00",
    "updated_at": "2020-12-22T15:35:06.262454+00:00",
    "version": 2,
    "expires": None
}
processor = Processor.parse_obj(example_object)
processor.version = 3 # This should be allowed
processor.created_at = datetime.utcnow().isoformat() # This I want to fail

Related but not what I am looking for - GitHub: Is there a way to have a single field be static and immutable with pydantic


I ended up solving this via the answer to the following issue: https://github.com/samuelcolvin/pydantic/issues/2217

1 Answers

The following does not work:

You can use a private attribute coupled with a property decorator. The private attribute is not includes in the models fields, but it is accessible via the property.

from pydantic import BaseModel, PrivateAttr


class Processor(BaseModel):
    """ Pydantic model for a Processor object within the database. """
    class Config:
        """
        Model Configuration: https://pydantic-docs.helpmanual.io/usage/model_config/
        """
        extra = 'forbid'
        allow_mutation = True # This allows mutation, cool
        validate_assignment = True
        underscore_attrs_are_private = True
    model: str
    _created_at: str = PrivateAttr(default_factory=datetime.utcnow().isoformat)
    _is_date_set: bool = PrivateAttr(default_factory=lambda: False)
    updated_at: str
    version: int = 0
    expires: Optional[int] = None

    @property
    def created_at(self):
        return self._created_at

    @create_at.setter
    def created_at(self, val):
        if self._is_date_set:
            raise AttributeError('The created_at attribute has already been set')
        self._is_date_set = True
        self._created_at = x
Related