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:
Then lets say you have main.py containing:
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:
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:
- a.py imports b.py
- while importing b.py, Python sees b.py imports a.py
- but a.py is not done loading yet, so Python only has a “half-built” version of a (a.py)
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:
Then assume b.py contains:
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.
- a.py imports b.py
- b.py imports c.py
- c.py imports a.py
For instance, a.py might contain:
b.py contains:
c.py contains:
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:
Next we change b.py depend on a.py in a way that doesn’t loop at the top level:
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.