Pydantic Private Fields (or Attributes)

Let’s say we have a simple Pydantic model that looks like this:

from pydantic import BaseModel

class Post(BaseModel):
    id: int
    title: str
    body: str

data = {
    'id': 1,
    'title': 'First Post',
    'body': 'Post Body',
}

post = Post(**data)
print(post) # id=1 title='First Post' body='Post Body'

When you instantiate a Pydantic model – post = Post(**data) – all the required attributes defined must be passed, i.e., id, title, body. What if you had one or more of the following requirements:

  • Prevent an attribute from being part of the model’s schema (post.schema|dict|json() should not have the attribute).
  • Have some instance member that is auto-generated or computed and not explicitly set while instantiating the model. This member may be shared between methods inside the model (a Pydantic model is just a Python class where you could define a lot of methods to perform required operations and share data between them).

This would mostly require us to have an attribute that is super internal or private to the model, i.e., we don’t set them explicitly.

If we simply add a new field or attribute to the model and try initialising it without explicitly passing a value for it, we will get the following error:

class Post(BaseModel):
    id: int
    title: str
    body: str
    created_at: datetime

# We do not pass `created_at`
data = { 'id': ..., 'title': ..., 'body': ... }

post = Post(**data)

# Following error will be raised
# pydantic.error_wrappers.ValidationError: 1 validation error for Post
# created_at
#   field required (type=value_error.missing)

We could make the attribute optional – created_at: Optional[datetime] – but that would still make it a part of post.schema().

So how can we have an attribute in our model definition that is not a part of the schema and remains completely “internal” or “private”? With PrivateAttr.

PrivateAttr allows us to add internal/private attributes to our model instance. The attribute names must be prefixed with a single or double underscore.

from pydantic import BaseModel, PrivateAttr
from datetime import datetime

class Post(BaseModel):
    id: int
    title: str
    body: str
    _created_at: datetime = PrivateAttr() # sunder or dunder name

    def __init__(self, **data):
        super().__init__(**data)
        # We generate the value for our private attribute
        self._created_at = datetime.now()

data = {
    'id': 1,
    'title': 'First Post',
    'body': 'Post Body',
}

post = Post(**data)
print(post) # id=1 title='First Post' body='Post Body'
print(post._created_at) # 2022-12-26 11:36:41.989699

Note: If we don’t prefix your private attribute with a single or double underscore (created_at instead of _created_at), then the following exception will be thrown:

NameError: Private attributes "created_at" must not be a valid field name; Use sunder or dunder names, e. g. "_created_at" or "__created_at__"

At this point, some people may wonder, what if we wanted to access the value of _created_at on the model instance without using the prefixed underscores (i.e., as created_at)? Would that be possible? Sure, we can do that with the @property decorator, i.e., by creating a managed attribute or property.

...

class Post(BaseModel):
    ...
    _created_at: datetime = PrivateAttr()

    def __init__(self, **data):
        super().__init__(**data)
        self._created_at = datetime.now()
    
    @property
    def created_at(self):
        return self._created_at

data = {
    ...
}

post = Post(**data)
print(post) # id=1 title='First Post' body='Post Body'
print(post._created_at) # 2022-12-26 11:36:41.989699
print(post.created_at) # 2022-12-26 11:36:41.989699

Bonus: If you always want fields inside your model starting with an underscore to be treated as private attributes, you can do so via config:

class Post(BaseModel):
    ...
    class Config:
        underscore_attrs_are_private = True

Leave a Reply

Your email address will not be published. Required fields are marked *