Have you ever thought of the paradoxes in life such as which came first, the egg or the chicken? In software also, there are such paradoxical problems that occur during development. One such paradox is the circular dependency due to imports. What is exactly a circular dependency and how we can avoid that dilemma to a machine so that it can execute it sequentially? Let us deep dive in those things with this post.
What is a circular dependency?
A circular dependency is similar to a race condition in the operating system thread execution. This is caused due to the imports happened in a program that are modularly separated. Let us see with an example.
In the above figure, the program is executed in a sequential way where the code pass the parameters to a function in B and then from B it is passing to another function in C. The return order will also be in the reverse direction.
But in circular dependency, if the function written in B requires C and if the function in C requires a function written in B, the machine gets confused on which to be executed first. As a result, it throws an error stating that circular imports could not be resolved.
# user.py
from fastapi import FastAPI
from pydantic import BaseModel
from post import Post
app = FastAPI()
class User(BaseModel):
id: int
name: str
posts: list[Post]
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# Fetch user and return
pass
# post.py
from fastapi import FastAPI
from pydantic import BaseModel
from user import User
app = FastAPI()
class Post(BaseModel):
id: int
title: str
author: User
@app.get("/posts/{post_id}")
async def get_post(post_id: int):
# Fetch post and return
pass
How to avoid circular dependencies?
Using ForwardRef in FastAPI applications:
# user.py
from fastapi import FastAPI
from pydantic import BaseModel, ForwardRef
app = FastAPI()
class User(BaseModel):
id: int
name: str
posts: list[ForwardRef('Post')] = []
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# Fetch user and return
pass
# post.py
from fastapi import FastAPI
from pydantic import BaseModel, ForwardRef
from user import User
app = FastAPI()
class Post(BaseModel):
id: int
title: str
author: ForwardRef('User')
Post.model_rebuild()
User.model_rebuild()
@app.get("/posts/{post_id}")
async def get_post(post_id: int):
# Fetch post and return
pass
Here, we are using Pydantic's ForwardRef class to define the models and utilizing model_rebuild() function to clear the circular dependencies happening by redefining the schemas during runtime execution.
Organization of modules and functions:
An evergreen proverb states that prevention is better than cure. Before circular dependency hits, it is better to organize the functions and define where it should be called. Proper calling of functions and proper organization of functions totally avoid such qualms to a machine and the execution happens smoothly.
Moving of shared code into a separate module:
If a function is required by two or other modules from different sources, it is better to move the common function into a separate module. This avoids race conditions within the program and allows better readability.
Conclusion
Thus, these are some of the methods through which we can solve and avoid such issues happening during development. What are the other methods we can use to solve this? Please ping in the comments section so that I can also learn and improve myself.
Thank you to all viewers for reading this article so patiently. I hope you would have derived something out of it!!
Top comments (0)