17

I am trying to create a dynamic model using Python's pydantic library. My input data is a regular dict. However, the content of the dict (read: its keys) may vary.
I am wondering how to dynamically create a pydantic model which is dependent on the dict's content?

I created a toy example with two different dicts (inputs1 and inputs2). Let's assume the nested dict called strategy may be different. Based on strategy/name I know in advance which fields will exist in strategy. I need to create the pydantic model based on strategy/name.

from pydantic import BaseModel

inputs1 = {
    "universe": {"name": "test_universe", "ccy": "USD"},
    "price_src": "csv",
    "strategy": {"name": "test_strat1"},
}
inputs2 = {
    "universe": {"name": "test_universe", "ccy": "USD"},
    "price_src": "csv",
    "strategy": {"name": "test_strat2", "periods": 10},
}


class Universe(BaseModel):
    name: str
    ccy: str = "EUR"


strategy_name = "test_strat2"

if strategy_name == "test_strat1":
    inputs = inputs1

    class Strategy(BaseModel):
        name: str


elif strategy_name == "test_strat2":
    inputs = inputs2

    class Strategy(BaseModel):
        name: str
        periods: int


class StaticModel(BaseModel):
    universe: Universe
    price_src: str = "csv"
    strategy: Strategy


static_model = StaticModel(**inputs)

My expected output if ``strategy_name == "test_strat1":

universe=Universe(name='test_universe', ccy='USD') price_src='csv' strategy=Strategy(name='test_strat1')

My expected output if ``strategy_name == "test_strat2":

universe=Universe(name='test_universe', ccy='USD') price_src='csv' strategy=Strategy(name='test_strat2', periods=10)

I was thinking about using pydantic's create_model function. However, I don't understand how to dynamically define the fields.

Andi
  • 3,196
  • 2
  • 24
  • 44

2 Answers2

10

For the dynamic creation of pydantic models, you can use create_model. Like so:

from pydantic import create_model

d = {"strategy": {"name": "test_strat2", "periods": 10}}

Strategy = create_model("Strategy", **d["strategy"])

print(Strategy.schema_json(indent=2))

Output:

{
  "title": "Strategy",
  "type": "object",
  "properties": {
    "name": {
      "title": "Name",
      "default": "test_strat2",
      "type": "string"
    },
    "periods": {
      "title": "Periods",
      "default": 10,
      "type": "integer"
    }
  }
}

alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • 1
    Do you mind to expand your solution with respect to the 1) How to define a ``str`` and ``int`` with no default values? and 2) Match my expected solutions in the way that ``strategy`` is nested. – Andi Feb 12 '21 at 09:05
  • Ad 1) ``Strategy = create_model("Strategy", **d)`` should give you the nested structure, shouldn't it? – Andi Feb 12 '21 at 09:13
  • I don't think it will be possible to create nested models, but you can experiment. As for removing the default values, you can try to use the syntax through the tuple specified in the documentation, like this `Strategy = create_model("Strategy", **{k: (type(v), ...) for k, v in d["strategy"].items()})` – alex_noname Feb 12 '21 at 12:21
2

You can use create_model with key=(type, ...) (3 dots) to declare a field without default value. For example:

from pydantic import BaseModel, create_model

...
if strategy_name == "test_strat1":
    inputs = inputs1
    Strategy = create_model('Strategy', name=(str, ...))

elif strategy_name == "test_strat2":
    inputs = inputs2
    Strategy = create_model('Strategy', name=(str, ...), periods=(int, ...))

print(Strategy.schema_json(indent=2))

Output for test_strat1:

{
  ...
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    }
  },
  "required": [
    "name"
  ]
}

And for test_strat2:

{
  ...
  "properties": {
    "name": {
      "title": "Name",
      "type": "string"
    },
    "periods": {
      "title": "Periods",
      "type": "integer"
    }
  },
  "required": [
    "name",
    "periods"
  ]
}

Related Pydantic documentation: https://pydantic-docs.helpmanual.io/usage/models/#dynamic-model-creation

You can see in the docs that:

  • foo=(str, ...) - str-typed foo attribute with no defaults
  • bar=123 - int-typed bar attribute with a default value of 123.
Khanh Luong
  • 418
  • 4
  • 7