Search code examples
pythonpydantic

How to set a Pydantic field value depending on other fields


from pydantic import BaseModel


class Grafana(BaseModel):
    user: str
    password: str
    host: str
    port: str
    api_key: str | None = None
    GRAFANA_URL = f"http://{user}:{password}@{host}:{port}"
    API_DATASOURCES = "/api/datasources"
    API_KEYS = "/api/auth/keys"

With Pydantic I get two unbound variables error messages for user, password, etc. in GRAFANA_URL.

Is there a way to solve this? In a regular class, I would just create GRAFANA_URL in the __init__ method. With Pydantic, I'm not sure how to proceed.


Solution

  • Pydantic v2

    In Pydantic version 2 you can define a computed field for this exact purpose.

    from pydantic import BaseModel, computed_field
    
    
    class Model(BaseModel):
        foo: str
        bar: str
    
        @computed_field
        @property
        def foobar(self) -> str:
            return self.foo + self.bar
    
    
    obj = Model(foo="a", bar="b")
    print(obj)  # foo='a' bar='b' foobar='ab'
    

    One nice thing about this is that foobar will be part of the the serialization schema, but not part of the validation schema. Roughly speaking, this means that the foobar field will be part of a model instance, when it is dumped/returned somewhere, but no foobar value is expected for constructing a model instance. This is very useful when for example generating OpenAPI documentations from your models.

    Demo, with the Model from above:

    ...
    
    import json
    
    schema_val = Model.model_json_schema(mode="validation")
    schema_ser = Model.model_json_schema(mode="serialization")
    print(json.dumps(schema_val, indent=4))
    print(json.dumps(schema_ser, indent=4))
    

    Output:

    {
        "properties": {
            "foo": {
                "title": "Foo",
                "type": "string"
            },
            "bar": {
                "title": "Bar",
                "type": "string"
            }
        },
        "required": [
            "foo",
            "bar"
        ],
        "title": "Model",
        "type": "object"
    }
    
    {
        "properties": {
            "foo": {
                "title": "Foo",
                "type": "string"
            },
            "bar": {
                "title": "Bar",
                "type": "string"
            },
            "foobar": {
                "readOnly": true,
                "title": "Foobar",
                "type": "string"
            }
        },
        "required": [
            "foo",
            "bar",
            "foobar"
        ],
        "title": "Model",
        "type": "object"
    }
    

    See the API reference for the computed_field decorator for additional options.


    Pydantic v1

    Option A: Use a @validator

    See the validators documentation for details.

    from typing import Any
    
    from pydantic import BaseModel, validator
    
    
    class Model(BaseModel):
        foo: str
        bar: str
        foobar: str = ""
    
        @validator("foobar", always=True)
        def set_if_empty(cls, v: str, values: dict[str, Any]) -> str:
            if v == "":
                return values["foo"] + values["bar"]
            return v
    
    
    obj = Model(foo="a", bar="b")
    print(obj)  # foo='a' bar='b' foobar='ab'
    

    That way foobar remains a regular model field.

    Note that for this to work, foobar must be defined after foo and bar. Otherwise you will have to use a root validator.

    PS: This approach also works analogously with Pydantic v2 @field_validator and @model_validator.

    Option B: Make it a @property

    from pydantic import BaseModel
    
    
    class Model(BaseModel):
        foo: str
        bar: str
    
        @property
        def foobar(self) -> str:
            return self.foo + self.bar
    
    
    obj = Model(foo="a", bar="b")
    print(obj)         # foo='a' bar='b'
    print(obj.foobar)  # ab
    

    Then foobar will not be a model field anymore and therefore not part of the schema. That may or may not be relevant to you.

    PS: This of course also works with Pydantic v2, though there probably is no benefit of using @property without @computed_field (see above).