sqlmodel icon indicating copy to clipboard operation
sqlmodel copied to clipboard

Cyclic import error with Multiple files

Open sebastianfym opened this issue 8 months ago • 7 comments

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 )

###############################################################

sebastianfym avatar Apr 29 '25 12:04 sebastianfym

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

Image

Image

bentoluizv avatar May 01 '25 14:05 bentoluizv

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

sebastianfym avatar May 05 '25 11:05 sebastianfym

Hi @sebastianfym

As you rightly mentioned:

"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."

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)

Kahacho avatar May 06 '25 03:05 Kahacho

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?

sebastianfym avatar May 06 '25 08:05 sebastianfym

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 avatar May 06 '25 09:05 Kahacho

@Kahacho, thanks a lot, it solved my problem.

sebastianfym avatar May 06 '25 09:05 sebastianfym

@sebastianfym Glad it worked out.

Kahacho avatar May 06 '25 09:05 Kahacho

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 NoInspectionAvailable error, because it is passed to sqlalchemy.inspect(), which doesn't support strings
  • importing the ORM models in main.py after a if 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_column in naming conventions
  • let SQLModel lazily use the link model (which is the spirit of the proposed solution here, but is not currently implemented)

thjungers avatar Jul 15 '25 09:07 thjungers

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

But then you would have to use a forward annotation everywhere you use the models right?

dollusion avatar Jul 28 '25 16:07 dollusion

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 avatar Jul 28 '25 19:07 Kahacho

@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?

thjungers avatar Jul 30 '25 07:07 thjungers

@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

YuriiMotov avatar Aug 06 '25 14:08 YuriiMotov