How to avoid circular imports in Python without merge into a single file

51 Views Asked by At

Here are my files:

- ./
    - child.py
    - parent.py
    - main.py

child.py

from dataclasses import dataclass
from parent import Parent

@dataclass
class Child:
    name: str

    def get_mother(self) -> Parent:
        # simulating query from database...
        return Parent(
            name="Jane",
            children=[
                Child(self.name),
                Child("Alice"),
                Child("Bob"),
            ],
        )

parent.py

from dataclasses import dataclass

from child import Child


@dataclass
class Parent:
    name: str
    children: list[Child]

main.py

from dataclasses import dataclass

from child import Child

if __name__ == "__main__":
    Charles = Child("Charles")
    print(f"{Charles}'s mother is {Charles.get_mother()}")

Question

How can I resolve this circular import issue without merge the 2 files.

Running main.py raises an exception

python main.py

Traceback (most recent call last):
  File "d:\circular_import\main.py", line 3, in <module>
    from child import Child

Then, I have changed from xx import XX to import xx in all 3 files. However, it does not resolve the issue.

python main.py

Traceback (most recent call last):
  File "d:\circular_import\main.py", line 3, in <module>
    import child
  File "d:\circular_import\child.py", line 3, in <module>
    import parent
  File "d:\circular_import\parent.py", line 7, in <module>
    class Parent:
  File "d:\circular_import\parent.py", line 9, in Parent
    children: list[child.Child]
AttributeError: partially initialized module 'child' has no attribute 'Child' (most likely due to a circular import)

If I merge parents.py and child.py into one file, it does solve the issue. But this is an less ideal solution.

python main.py

Child(name='Charles')'s mother is Parent(name='Jane', children=[Child(name='Charles'), Child(name='Alice'), Child(name='Bob')])
2

There are 2 best solutions below

3
Basilevs On

This is already answered

The idea is to do one of imports lazily from inside a method. This will require relaxing some type declarations though.

    def get_mother(self):
        from parent import Parent
        # simulating query from database...
        return Parent(
            name="Jane",
            children=[
                Child(self.name),
                Child("Alice"),
                Child("Bob"),
            ],
        )
0
mousetail On

Since you only use them for type hints you only need to import them when type checking.

from dataclasses import dataclass
import typing
if typing.TYPE_CHECKING:
    from child import Child

@dataclass
class Parent:
    name: str
    children: "list[Child]"

This will not create a circular import at runtime, but still type hint properly.

Child has a "genuine" dependency on parent so it will need a normal import