Python Circular Import

Salman Mehmood Oct 10, 2023
  1. Reproduce Circular Import Error in Python
  2. Solve Circular Import Error in Python
  3. Solve Circular Import Error Caused by Type Hint in Python
Python Circular Import

In this article, we will learn how circular imports commonly arise and the main ways to fix them. We will also see how to fix a circular import error caused by a type hint in python.

Reproduce Circular Import Error in Python

Have you ever come across an error when trying to import a module? In this case, we get an error that is shown below.

from My_Module_A import My_FUNC_A


def My_main_Func():
    My_FUNC_A()


if __name__ == "main":
    My_main_Func()

Output:

ImportError: cannot import name 'My_FUNC_A' from partially initialized module 'My_Module_A' (most likely due to a circular import)

It tells us that the most likely cause is a circular import. Here is the simplest and most common way that this kind of import cycle can happen at runtime main tried to import something from My_Module_A.

python circular import - example one

So, it starts initializing My_Module_A and running the code from My_Module_A, but the first line in My_Module_A is to import something from module MY_Module_B.

So, it stops initializing module My_Module_A because MY_Module_B has to be initialized first. And then, it hops over to MY_Module_B and starts running that code instead.

But, the first line in module MY_Module_B is needed something from My_Module_A, so it stops running MY_Module_B and goes over to My_Module_A.

And it would like to start initializing My_Module_A, but it realizes that My_Module_A is already in the initialization process. So that’s where the error occurs.

We can see this chain of events unfold in the traceback. Here we see in main.py; it tries to import function My_FUNC_A from module My_Module_A, which triggered the import of function MY_Func_B2 from module MY_Module_B.

And, MY_Module_B.py triggered the import of function My_FUNC_A again from module My_Module_A, where the final error occurred.

Traceback (most recent call last):
  File "c:\Users\Dell\Desktop\demo\main.py", line 1, in <module>
    from My_Module_A import My_FUNC_A
  File "c:\Users\Dell\Desktop\demo\My_Module_A.py", line 1, in <module>
    from MY_Module_B, import MY_Func_B2
  File "c:\Users\Dell\Desktop\demo\MY_Module_B.py", line 1, in <module>
    from My_Module_A import My_FUNC_A
ImportError: cannot import name 'My_FUNC_A' from partially initialized module 'My_Module_A' (most likely due to a circular import) (c:\Users\Dell\Desktop\demo\My_Module_A.py)

We have an import time cycle between the modules My_Module_A and MY_Module_B, but if you look at the names we imported, we do not use them at import time.

We use them when this function My_FUNC_A() or when this function MY_Func_B2() is called. Since we are not calling these functions at import time, there is no true cyclic dependency, which means we can eliminate this import cycle.

Solve Circular Import Error in Python

There are three common ways to resolve this issue; the first is to take your import and put it inside the function.

In python, imports can happen at any time, including when a function is called; if you need this function MY_Func_B1(), then it could be the import inside the My_FUNC_A() function.

def My_FUNC_A():
    from MY_Module_B import MY_Func_B1

    MY_Func_B1()

It could be even more efficient because if you never call the function My_FUNC_A(), then it may be that you never even have to import MY_Func_B1 at all. For example, let’s say you have a bunch of different functions in My_Module_A, and many different ones use this function MY_Func_B1().

In that case, the better solution would be to import the module MY_Module_B directly and in the functions where you use it, use the longer name like MY_Module_B.MY_Func_B1(). So go ahead and do this conversion for all modules involved in the cycle.

import MY_Module_B


def My_FUNC_A():
    MY_Module_B.MY_Func_B1()

Let’s see how this resolves the import cycle at runtime error. First, our My_main_Func() function tries to import something from My_Module_A, causing My_Module_A to start running. Then, in My_Module_A.py, we get to import module MY_Module_B, which triggers MY_Module_B to start running.

In MY_Module_B.py, when we get to the import module My_Module_A, the module has already started initializing; this module object technically exists. Since it exists, it does not begin rerunning this.

It just says, okay, that exists, so the MY_Module_B module will continue, finish its import, and then after that import is done, the My_Module_A module will finish importing.

python circular import - example two

Output:

15

The third and final common way to eliminate this import cycle is to merge the two modules. So, again, no imports, no import cycles; however, if you had two or maybe even more modules in the first place, you probably had them for a reason.

We are not just going to recommend that you take all of your modules and merge them into one; that would be silly. So, you should probably prefer using one of the first two methods.

Source code of My_Module_A.py file.

import MY_Module_B


def My_FUNC_A():
    print(MY_Module_B.MY_Func_B1())

Source code of MY_Module_B.py file.

import My_Module_A


def MY_Func_B1():
    return 5 + 10


def MY_Func_B2():
    My_Module_A.My_FUNC_A()

Source code of main.py file.

from My_Module_A import My_FUNC_A


def My_main_Func():
    My_FUNC_A()


if __name__ == "main":
    My_main_Func()

Solve Circular Import Error Caused by Type Hint in Python

Let’s look at another example, the following most common kind of import cycle you will run due to using type hints. This example is unlike the previous one because type annotations are defined on a function or class definition.

When the function is defined, or the class is defined but not defined when we run it, the most common use case of type hinting is not even at runtime. If all we care about is static analysis, then we do not even need to do the import at runtime.

The typing module provides this variable, TYPE_CHECKING is a false constant at runtime. So, if we put all the imports we need for type checking in one of these, then none will happen at runtime.

Avoiding the import loop altogether, if you import this class Mod_A_class, you will get a name error because this name Mod_A_class has not been imported.

python circular import - example three

Output:

Traceback (most recent call last):
  File "c:\Users\Dell\Desktop\demo\main.py", line 1, in <module>
    from My_Module_A import My_FUNC_A
  File "c:\Users\Dell\Desktop\demo\My_Module_A.py", line 9, in <module>
    from MY_Module_B, import MY_Func_B1
  File "c:\Users\Dell\Desktop\demo\MY_Module_B.py", line 22, in <module>
    class Mod_b_class:
  File "c:\Users\Dell\Desktop\demo\MY_Module_B.py", line 23, in Mod_b_class
    def __init__(self,a : Mod_A_class):
NameError: name 'Mod_A_class' is not defined

To fix this, add this from __future__ import annotations. What does this?

It changes the way annotations work; instead of evaluating Mod_b_class as a name, all annotations are converted to strings.

python circular import - example four

So now you do not get an error at runtime here because even though the name Mod_b_class is not imported, the string Mod_b_class certainly exists. However, you should be aware that whenever you do one of these from __future__ imports, it is not necessarily guaranteed behavior forever.

Now, this causes your annotations to be handled as strings, but there is a competing proposal to make them just be evaluated lazily. Hence, they are not evaluated unless you try to look at them and inspect your type hints at runtime.

This probably will not make any difference for your code base, but you should still be aware. An alternative solution is to again use import modules instead of from module imports, and you will still need the annotations from __future__.

python circular import - example five

Complete source code of module My_Module_A.py.

# from __future__ import annotations

# from typing import TYPE_CHECKING

# from MY_Module_B import MY_Func_B1

# if TYPE_CHECKING:
#     from MY_Module_B import Mod_b_class

# def My_FUNC_A():
#     b : Mod_b_class = MY_Func_B1()


# class Mod_A_class:
#     def __init__(self,b : Mod_b_class):
#         self.b=b


from __future__ import annotations
import MY_Module_B


def My_FUNC_A():
    b: MY_Module_B.Mod_b_class = MY_Module_B.MY_Func_B1()


class Mod_A_class:
    def __init__(self, b: MY_Module_B.Mod_b_class):
        self.b = b

Complete source code of module MY_Module_B.py.

from __future__ import annotations

import My_Module_A


def MY_Func_B1():
    ...


class Mod_b_class:
    def __init__(self, a: My_Module_A.Mod_A_class):
        self.a = a

Complete source code of main.py file.

from My_Module_A import My_FUNC_A


def My_main_Func():
    My_FUNC_A()


if __name__ == "main":
    My_main_Func()
Salman Mehmood avatar Salman Mehmood avatar

Hello! I am Salman Bin Mehmood(Baum), a software developer and I help organizations, address complex problems. My expertise lies within back-end, data science and machine learning. I am a lifelong learner, currently working on metaverse, and enrolled in a course building an AI application with python. I love solving problems and developing bug-free software for people. I write content related to python and hot Technologies.

LinkedIn

Related Article - Python Import