Python

How to Fix Circular Imports in Python

Learn what circular imports are in Python, how they cause ImportError and AttributeError, and the best ways to fix them — from lazy imports to module restructuring.

August 2026 5 min read 10 views 0 hearts

I've been writing Python code for years, and let me tell you — circular imports are one of those errors that make you want to throw your laptop out the window. You import something, run your script, and boom: ImportError. Or worse, you get a weird AttributeError that makes no sense at all. I've seen developers spend hours debugging these, and it's usually because circular imports behave differently depending on how you run your code.

Let me walk you through what circular imports actually are, how to spot them, and the real tricks to fixing them without ripping your entire project apart.

What's actually happening here?

A circular import happens when module A imports module B, and module B imports module A (or A imports B, B imports C, and C imports A). Python tries to resolve this by partially loading modules, which creates a mess. Imagine you're building a website with PythonSkillset — you have user profiles and blog posts. Your User module imports Post to show user's posts, and Post module imports User to show who wrote it. That's your circular import brewing.

The sneaky way circular imports reveal themselves

The classic symptom is getting an ImportError when you run your code, but here's the thing — sometimes it only fails when you call a specific function. I once spent an hour debugging a script that worked fine in tests but crashed when deployed. Turns out, the circular import only hit when a certain endpoint was called because the import chain went through a different path.

Let's fix this properly

1. Move imports inside functions (the lazy import trick)

This is my go-to fix for quick patches. Instead of importing at the top of your file, import inside the function that actually needs it.

# module_a.py
def get_user_posts(user_id):
    from module_b import Post  # Import happens only when called
    # your code here

This works because Python only executes the import when the function runs, not when the module loads. By then, both modules exist. Just be careful — this can slow things down if you call the function many times.

2. Restructure your modules (the real solution)

Sometimes the best fix is stepping back. Ask yourself: does module A really need module B, and module B need module A? Often, both depend on something shared.

At PythonSkillset, we ran into this with our payment processing system. We had Order module importing Invoice, and Invoice importing Order. The fix? Create a shared_models.py that both import from. Now Order and Invoice never touch each other directly.

# shared_models.py
class BaseOrder:
    # common stuff

class BaseInvoice:
    # common stuff

# order.py
from shared_models import BaseOrder

# invoice.py
from shared_models import BaseInvoice

3. Use init.py cleverly

Your package's __init__.py can be both a blessing and a curse. Circular imports often happen because __init__.py imports everything upfront. Try importing lazily there too:

# mypackage/__init__.py
# Don't do this:
# from .submodule_a import SomeClass
# from .submodule_b import AnotherClass

# Instead, let submodules import what they need directly

4. The dependency inversion trick

If two modules need each other's functions, create an interface or abstract class they both use. This is more work, but it makes your code cleaner and breaks the cycle permanently.

# utils.py
class DataProvider:
    def get_user(self, id):
        raise NotImplementedError

# module_a.py
from utils import DataProvider

class UserProvider(DataProvider):
    def get_user(self, id):
        # actual implementation
        pass

# module_b.py
# Now module_b can import DataProvider without knowing about module_a

What not to do

I've seen people try wild things like renaming imports with import module_a as a_different_name — doesn't fix anything. Or deleting imports hoping the code still works. Don't do that. Also, avoid using sys.path hacks unless you really know what you're doing. It's like putting duct tape on a leaking pipe — it won't hold.

Real talk: when to worry

Not every circular import is bad. Sometimes your design genuinely requires two-way communication. But if you're hitting circular imports often, your code structure probably needs a rethink. I've found that drawing a dependency graph (even on paper) helps spot these cycles before they become problems.

The next time Python throws that circular import error at you, take a breath. Start with the lazy import fix if you're in a hurry, but plan to refactor later. And remember — PythonSkillset's codebase went through three major refactors before we got our import structure right. It's normal.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.