To me, the other answers don't seem to solve this on a satisfactory level due to ignoring the locals in modules. Here is a straightforward way to make that work on separate files:
user.py
from typing import TYPE_CHECKING, List
from pydantic import BaseModel
if TYPE_CHECKING:
from project import Project
class User(BaseModel):
projects: List['Project']
project.py
from typing import TYPE_CHECKING, List
from pydantic import BaseModel
if TYPE_CHECKING:
from user import User
class Project(BaseModel):
members: List['User']
main.py
from project import Project
from user import User
# Update the references that are as strings
Project.update_forward_refs(User=User)
User.update_forward_refs(Project=Project)
# Example: Projects into User and Users into Project
Project(
members=[
User(
projects=[
Project(members=[])
]
)
]
)
This works if you run the main.py
. If you are building a package, you may put that content to an __init__.py
file that is high enough in the structure to not have circular import problem.
Note how we passed the User=User
and Project=Project
to update_forward_refs
. This is because the module scopes where these classes are don't have references to each other (as if they did, there would be circular import). Therefore we pass them in main.py when updating the references as there we don't have the circular import problem.
NOTE: About type checking
If if TYPE_CHECKING:
patterns are unfamiliar, they are basically if blocks that are never True on runtime (running your code) but they are used by code analysis (IDEs) to highlight the types. Those blocks are not needed for the example to work but are highly recommended as otherwise, it's hard to read the code, find out where these classes actually are defined and fully utilize code analysis tools.