Kodeclik Blog
Python Circular Imports (and how to fix them)
In Python, an import is how you use code from another file (or package). When you write import something, Python goes and finds something, loads it, and then runs the code in that file from top to bottom. After that, Python remembers it so it doesn’t need to run it again the next time you import it.
Example of a Python import
Here’s a tiny example. Imagine you have two files in the same folder, first greetings.py:
def say_hi(name):
return f"Hi, {name}!"Then lets say you have main.py containing:
import greetings
print(greetings.say_hi("Ava"))When you run main.py, Python loads greetings.py, runs it (it defines say_hi), and then main.py can call greetings.say_hi(...).
You might also see this style:
from greetings import say_hi
print(say_hi("Ava"))Both are valid. The first one imports the module and you access names with greetings.say_hi. The second one imports a name directly into your file.
What is a circular import?
A circular import is when Python gets stuck in a loop while importing.
The simplest loop looks like this:
That “half-built module” is the heart of the problem. Sometimes the import fails immediately with an error like “cannot import name … from partially initialized module …”. Other times it seems to import, but later you get an AttributeError because the thing you expected inside the module was never created yet.
A good mental picture is: importing is like reading a recipe from top to bottom. If page 1 tells you “go read page 2 first,” and page 2 tells you “go read page 1 first,” you never finish either page.
Direct mutual imports
A direct mutual import is when two files import each other directly.
Consider a file a.py containing:
from b import greet_from_b
def greet_from_a():
return "Hello from A"
def call_b():
return greet_from_b()Then assume b.py contains:
from a import greet_from_a
def greet_from_b():
return "Hello from B"
def call_a():
return greet_from_a()Now run either file (or import one of them in a third file). Python tries to import a, which imports b, which imports a again. But a isn’t finished defining greet_from_a yet, because it paused in the middle to import b. That’s when Python may complain that it can’t import a name from a partially initialized module.
A really important detail: the problem is usually caused by imports at the top level of a file (imports that happen as soon as the file starts being loaded). If two files need each other at the top level, you have a loop.
Indirect mutual imports
An indirect mutual import is the same loop, but it goes through more than two files, so it’s harder to spot.
For instance, a.py might contain:
from b import b_message
def a_message():
return "A says hi"
def use_b():
return b_message()b.py contains:
from c import c_message
def b_message():
return "B here"
def use_c():
return c_message()c.py contains:
from a import a_message
def c_message():
return "C checking in"
def use_a():
return a_message()This one feels unfair because no file imports itself directly, but the loop still exists. Python still has to load all three, and during that process it gets sent back to a.py before a.py finishes loading the first time.
Fixing circular imports
There isn’t one “magic” fix because circular imports usually happen for a reason: the code is tangled. The goal is to untangle it so that imports go in one direction.
For instance, if you only need the other module in one function, you can import it inside that function. That delays the import until the moment the function is called, instead of doing it immediately when the file loads.
Let’s fix the earlier direct example by changing a.py like this:
def greet_from_a():
return "Hello from A"
def call_b():
from b import greet_from_b
return greet_from_b()Next we change b.py depend on a.py in a way that doesn’t loop at the top level:
from a import greet_from_a
def greet_from_b():
return "Hello from B"
def call_a():
return greet_from_a()Now when Python imports a.py, it does not immediately import b.py. The import of b happens later, only when call_b() runs.
This is often the quickest practical fix, especially in small scripts. It’s also common in bigger codebases when two modules must talk to each other, but only at runtime.
In general, when you write from b import greet_from_b, Python tries to grab that name immediately during import time. If b isn’t fully loaded yet, it can fail. Thus, this approach does not solve every circular import, but it can reduce the “partially initialized name” problem because you are not trying to pull a specific function out of a module before it’s ready.
Summary
Circular imports happen when Python files get stuck importing each other in a loop, usually because imports run code immediately from top to bottom and a module can be “half loaded” when it gets imported again. We have seen one potential fix. But there are many other fixes that could be explored (but beyond the scope of this article).
Enjoy this blogpost? Want to learn Python with us? Sign up for 1:1 or small group classes.