Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Declaring computed python-level property in pydantic

I have a class deriving from pydantic.BaseModel and would like to create a "fake" attribute, i.e. a computed property. The propery keyword does not seem to work with Pydantic the usual way. Below is the MWE, where the class stores value and defines read/write property called half with the obvious meaning. Reading the property works fine with Pydantic, but the assignment fails.

I know Pydantic is modifying low-level details of attribute access; perhaps there is a way to define computed field in Pydantic in a different way?

import pydantic

class Object(object):
    def __init__(self,*,value): self.value=value
    half=property(lambda self: .5*self.value,lambda self,h: setattr(self,'value',h*2))

class Pydantic(pydantic.BaseModel):
    class Config:
        extra='allow'
    value: float
    half=property(lambda self: .5*self.value,lambda self,h: setattr(self,'value',h*2))

o,p=Object(value=1.),Pydantic(value=1.)
print(o.half,p.half)
o.half=p.half=2
print(o.value,p.value)

outputs (value=1. was not modified by assigning half in the Pydantic case):

0.5 0.5
4 1.0
like image 455
eudoxos Avatar asked Feb 05 '26 19:02

eudoxos


2 Answers

I happened to be working on the same problem today. Officially it is not supported yet, as discussed here.

However, I did find the following example which works well:

class Person(BaseModel):
    first_name: str
    last_name: str
    full_name: str = None

    @validator("full_name", always=True)
    def composite_name(cls, v, values, **kwargs):
        return f"{values['first_name']} {values['last_name']}"

Do make sure your derived field comes after the fields you want to derive it from, else the values dict will not contain the needed values (e.g. full_name comes after first_name and last_name that need to be fetched from values).

UPDATE: As Hyagoro mentions below, there is now an officially supported computed_field decorator in Pydantic 2. Unfortunately, that feature won't be backported to Pydantic v1 (Source). So in Pydantic v2 the above example becomes:

from pydantic import BaseModel, computed_field

class Person(BaseModel):
    first_name: str
    last_name: str
    full_name: str = None

    @computed_field
    @property    
    def composite_name(self):
        return f"{self.first_name} {self.last_name}"
like image 63
benvdh Avatar answered Feb 08 '26 20:02

benvdh


Instead of using a property, here's an example which shows how to use pydantic.root_validator to compute the value of an optional field: https://daniellenz.blog/2021/02/20/computed-fields-in-pydantic/

I've adapted this for a similar application:

class Section (BaseModel):
    title: constr(strip_whitespace=True)
    chunks: conlist(min_items=1, item_type=Chunk)
    size: typing.Optional[ PositiveInt ] = None
    role: typing.Optional[ typing.List[ str ]] = []
    license: constr(strip_whitespace=True)

    @root_validator
    def compute_size (cls, values) -> typing.Dict:
        if values["size"] is None:
            values["size"] = sum([
                chunk.get_size()
                for chunk in values["chunks"]
            ])

        return values

In this case each element of the discriminated union chunks has a get_size() method to compute its size. If the size field isn't specified explicitly in serialization (e.g., input from a JSON file) then it gets computed.

like image 39
Paco Avatar answered Feb 08 '26 21:02

Paco