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.
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.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.