How to correctly structure pydantic models?

Viewed 975

I am trying to structure the code for my data model using pydantic but I am running into an apparently insolvable problem.

The code below works as expected:

main.py

#
from __future__ import annotations
#
from pydantic import BaseModel
#
class Item(BaseModel):
    tag: Tag = None
    class Config:
        orm_mode = True

class Tag(BaseModel):
    item: Item = None
    class Config:
        orm_mode = True


Item.update_forward_refs()
Tag.update_forward_refs()

i = Item()
t = Tag()

i.tag = t
t.item = i
# print(i) fails due to cyclic model but this is another story

Then I am trying to refactor my code like this:

main.py
model/item.py
model/tag.py

with

main.py:

#
from model.item import Item
from model.tag import Tag
#
Item.update_forward_refs()
Tag.update_forward_refs()
#
i = Item()
t = Tag()
#
i.tag = t
t.item = i

model/item.py:

#
from __future__ import annotations
#
from pydantic import BaseModel
#
from .tag import Tag
#
class Item(BaseModel):
    tag: Tag = None
    class Config:
        orm_mode = True

model/tag.py:

#
from __future__ import annotations
#
from pydantic import BaseModel
#
from .item import Item
#
class Tag(BaseModel):
    item: Item = None
    class Config:
        orm_mode = True

This code fails on import because of the cross reference of tag to item and vice versa. But if I remove the imports from the model files:


    from .item import Item

    from .tag import Tag

The code fails again complaining about missing Tag or Item:

NameError: name 'Tag' is not defined 
NameError: name 'Item' is not defined 

All other attempts failed, like using explicit reference model.Item instead of Item.

What is the correct way to get around this, preferably keeping the nice symmetry of the code ?

1 Answers

Just to share an alternative option - convtools models (docs | github), which is pretty young, but it handles cycles:

from typing import Optional

from convtools.contrib.models import DictModel, build

class Item(DictModel):
    tag: Optional["Tag"] = None

class Tag(DictModel):
    item: Optional[Item] = None

item_data = {"tag": None}
tag_data = {"item": item_data}
item_data["tag"] = tag_data

obj, errors = build(Item, item_data)
"""
>>> In [2]: obj
>>> Out[2]: Item(tag=Tag(item=<convtools.contrib.models.type_handlers.ProxyItem object at 0x103428a60>))
>>> 
>>> In [3]: obj.tag
>>> Out[3]: Tag(item=<convtools.contrib.models.type_handlers.ProxyItem object at 0x103428a60>)
>>> 
>>> In [4]: obj.tag.item
>>> Out[4]: <convtools.contrib.models.type_handlers.ProxyItem object at 0x103428a60>
>>> 
>>> In [5]: obj.tag.item.tag
>>> Out[5]: Tag(item=<convtools.contrib.models.type_handlers.ProxyItem object at 0x103428a60>)
"""
Related