Kodeclik Logo

Learn More

Fall 2025

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.

Python circular imports

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.

Kodeclik sidebar newsletter

Join our mailing list

Subscribe to get updates about our classes, camps, coupons, and more.

About

Kodeclik is an online coding academy for kids and teens to learn real world programming. Kids are introduced to coding in a fun and exciting way and are challeged to higher levels with engaging, high quality content.

Copyright @ Kodeclik 2025. All rights reserved.