Cyclic import error with Multiple files
Privileged issue
- [x] I'm @tiangolo or he asked me directly to create an issue here.
Issue Content
At the moment, if you split the model file into different folders, the cyclic import error will appear.
Decision:
from future import annotations
if TYPE_CHECKING: from models.general_name_models import Model1, Model2
it did not produce results, because when accessing models imported under if TYPE_CHECKING, they are considered strings, and in this case, the Relationship does not work.
Therefore, in order for all the imports to work adequately and there were no problems with requests, we had to remove all the Relationship's.
If all the Relationships are removed, then the project starts and even functions (partially), but in this case we will have to rewrite most of the functions from db , since in many places we join as follows (example): joinedload(Team.hero)
This query won't work because the Team model won't see the hero attribute.
Otherwise, if you don't choose a Relationship (for example, a team.hero), you can get them yourself: sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[Hero(hero)] the expression "relationship("list['Team']")" appears to use a common class as an argument for relationship(); please specify a common argument using an annotation, such as "team: Mapped[list['Team']] = communication()"
Below are some examples of my code.
link.py: class HeroTeamLink(SQLModel, table=True): hero_id: int = Field(foreign_key="hero.id", primary_key=True) team_id: int = Field(foreign_key="team.id", primary_key=True)
############################################################### team.py: from future import annotations from typing import TYPE_CHECKING
from sqlmodel import ( Relationship, SQLModel, )
if TYPE_CHECKING: from models.hero import Hero
class Team(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str heroes: list["Hero"] = Relationship( # secondary="hero", back_populates="team", link_model=HeroTeamLink, )
############################################################### hero.py: from future import annotations from typing import TYPE_CHECKING
from sqlmodel import ( Relationship, SQLModel, )
if TYPE_CHECKING: from models.team import Team from models.link import HeroTeamLink
class Hero(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str teams: list["Team"] = Relationship( back_populates="hero", link_model=HeroTeamLink )
###############################################################
Hi @sebastianfym
I solved this cyclic import problem by moving the import to the end of the file and calling pydantic's model_rebuild method.
You can read more in the Pydantic docs
Hi @bentoluizv, thank you, your solution works, but in my case it is used many-to-many, and at this point an error occurs.
I have 3 files, each of which has models. Here is my service file:
from models.link_models import ServiceQualificationLink
class Service(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
qualification: list["Qualification"] = Relationship(
back_populates="service",
# link_model=ServiceQualificationLink,
)
from models.qualifications import Qualification
Service.model_rebuild()
_*_*_*_*_*_*_*_** Here is my qualifications file:
from models.link_models import ServiceQualificationLink
class Qualification(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
service: list["Service"] = Relationship(
back_populates="qualification" ,
# link_model=ServiceQualificationLink
)
from models.services import Service
Qualification.model_rebuild()
_*_*_*_*_*_*_*_** This is my link_model_file
from sqlmodel import Field, SQLModel
class ServiceQualificationLink(SQLModel, table=True):
service_id: int | None = Field(default=None, foreign_key="services.id", primary_key=True)
qualification_id: int | None = Field(
default=None, foreign_key="qualifications.id", primary_key=True
)
_*_*_*_*_*_*_*_**
If I have a commented out link_model string, then I get the following error:
sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper[Qualification(qualifications)], expression "relationship("list['Service']")" seems to be using a generic class as the argument to relationship(); please state the generic argument using an annotation, e.g. "service: Mapped[list['Service']] = relationship()"
If this field is not commented out, then the error is already like this:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'Mapper[Qualification(qualifications)]'. Original exception was: When initializing mapper Mapper[Qualification(qualifications)], expression "relationship("list['Service']")" seems to be using a generic class as the argument to relationship(); please state the generic argument using an annotation, e.g. "service: Mapped[list['Service']] = relationship()"
Here are the versions of the libraries that I use: SQLAlchemy = 2.0.40 sqlmodel = 0.0.24 Python 3.13.1
Hi @sebastianfym
As you rightly mentioned:
And that is why while annotating qualification here:
qualification: list["Qualification"]
You have defined Qualification as a literal string by adding quotation marks "Qualification"
However, when accessing the link_model - ServiceQualificationLink - in Relationship, you have not defined it as a literal string. To resolve your issue, have it as:
qualification: list["Qualification"] = Relationship(
back_populates="service",
link_model="ServiceQualificationLink"
)
and that should solve your issue.
I reproduced your previous code hero example as follows and it worked perfectly:
team.py
from typing import TYPE_CHECKING
from sqlmodel import Relationship, SQLModel, Field
if TYPE_CHECKING:
from .hero import Hero
from .link import HeroTeamLink
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: list["Hero"] = Relationship(
# secondary="hero",
back_populates="team",
link_model="HeroTeamLink",
)
hero.py
from typing import TYPE_CHECKING
from sqlmodel import Relationship, SQLModel, Field
if TYPE_CHECKING:
from .team import Team
from .link import HeroTeamLink
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
teams: list["Team"] = Relationship(back_populates="hero", link_model="HeroTeamLink")
link.py
from sqlmodel import SQLModel, Field
class HeroTeamLink(SQLModel, table=True):
hero_id: int = Field(foreign_key="hero.id", primary_key=True)
team_id: int = Field(foreign_key="team.id", primary_key=True)
Hi @Kahacho, thank you for your help. I used your code examples, but I got the following error:
Traceback (most recent call last):
File "/Users/bogdanvelikorodov/projects/test_1/main.py", line 4, in <module>
from hero import Hero
File "/Users/bogdanvelikorodov/projects/test_1/hero.py", line 10, in <module>
class Hero(SQLModel, table=True):
...<2 lines>...
teams: list["Team"] = Relationship(back_populates="hero", link_model="HeroTeamLink")
File "/Users/bogdanvelikorodov/projects/test_1/.venv/lib/python3.13/site-packages/sqlmodel/main.py", line 623, in __init__
ins = inspect(rel_info.link_model)
File "/Users/bogdanvelikorodov/projects/test_1/.venv/lib/python3.13/site-packages/sqlalchemy/inspection.py", line 147, in inspect
raise exc.NoInspectionAvailable(
...<2 lines>...
)
sqlalchemy.exc.NoInspectionAvailable: No inspection system is available for object of type <class 'str'>
Here are the code examples: *_*_*_*_*__* hero.py
from typing import TYPE_CHECKING
from sqlmodel import Relationship, SQLModel, Field
if TYPE_CHECKING:
from .team import Team
from .link import HeroTeamLink
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
teams: list["Team"] = Relationship(back_populates="hero", link_model="HeroTeamLink")
*_*_*_*_*__*
team.py
from typing import TYPE_CHECKING
from sqlmodel import Relationship, SQLModel, Field
if TYPE_CHECKING:
from .hero import Hero
from .link import HeroTeamLink
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: list["Hero"] = Relationship(
# secondary="hero",
back_populates="team",
link_model="HeroTeamLink",
)
*_*_*_*_*__*
link.py
from sqlmodel import SQLModel, Field
class HeroTeamLink(SQLModel, table=True):
hero_id: int = Field(foreign_key="hero.id", primary_key=True)
team_id: int = Field(foreign_key="team.id", primary_key=True)
Can you tell me your library versions and what could I have reproduced wrong?
Hi @sebastianfym
In your main.py, use TYPE_CHECKING as well and import as follows:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from team import Team
from hero import Hero
@Kahacho, thanks a lot, it solved my problem.
@sebastianfym Glad it worked out.
Hello,
I was having the same issue (defining the link model of a many-to-many relationship into a different file leads to circular imports), but I don't understand how the proposed solution solves the issue.
Taking the example from above:
- putting the link_model argument between quotes leads to a
NoInspectionAvailableerror, because it is passed tosqlalchemy.inspect(), which doesn't support strings - importing the ORM models in
main.pyafter aif TYPE_CHECKING:bypasses the error, but the models are then not imported at runtime and are not usable.
For future reference, the solution is actually straightforward: import the HeroTeamLink model in both hero.py and team.py, but don't import the Hero and Team models in link.py. Then, there is no circular import and SQLAlchemy is smart enough to defer the lookup of the table referenced in the foreign keys.
EXCEPT if you define naming conventions (SQLModel.metadata.naming_convention) that include a referred_column (which I did). In that case, SQLAlchemy needs to lookup that table immediately, which is only possible if the related model is imported in the file. But doing so leads to circular imports.
Solutions to avoid this:
- have SQLAlchemy change the way they handle this naming convention
- put the two related models and the link model in the same file
- don't use
referred_columnin naming conventions - let SQLModel lazily use the link model (which is the spirit of the proposed solution here, but is not currently implemented)
In your
main.py, use TYPE_CHECKING as well and import as follows:from typing import TYPE_CHECKING
if TYPE_CHECKING: from team import Team from hero import Hero
But then you would have to use a forward annotation everywhere you use the models right?
Hi @lucasfelber, yes. For additional context, I recommend reading the-future-of-fastapi-and-pydantic-is-bright by @tiangolo. It provides valuable insight into the reasoning behind the changes and the approach I previously suggested. @thjungers, it might also be helpful for you to review it, as it directly relates to the solution I previously suggested.
@Kahacho thanks for the interesting read.
I need to instantiate the Team and Hero objects in my main.py file, so I need to import the actual classes at runtime, thus my comment. But maybe in your answer you only need annotations?
@sebastianfym, @thjungers, Could you please check if this works for you?
See code in details:
main.py
from sqlmodel import Session, SQLModel, create_engine, select
from .hero import Hero
from .team import Team
def test_():
h1 = Hero(name="me")
t1 = Team(name="team 1")
h1.teams.append(t1)
engine = create_engine("sqlite://")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
session.add(h1)
session.add(t1)
session.commit()
with Session(engine) as session:
query_hero = session.scalar(
select(Hero).limit(1)
)
assert query_hero
assert query_hero.id == 1
assert query_hero.name == "me"
assert query_hero.teams[0].name == "team 1"
hero.py
from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, SQLModel
from .link import HeroTeamLink
if TYPE_CHECKING:
from team import Team
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
teams: list["Team"] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
team.py
from typing import TYPE_CHECKING
from sqlmodel import Field, Relationship, SQLModel
from .link import HeroTeamLink
if TYPE_CHECKING:
from .hero import Hero
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
heroes: list["Hero"] = Relationship(
back_populates="teams",
link_model=HeroTeamLink,
)
link.py
from sqlmodel import Field, SQLModel
class HeroTeamLink(SQLModel, table=True):
hero_id: int = Field(foreign_key="hero.id", primary_key=True)
team_id: int = Field(foreign_key="team.id", primary_key=True)
All files in the archive: files.zip