On this page

Engineering6 min read

Fix Python Circular Import Error — What It Is and How to Resolve It

Python circular imports cause ImportError or partially-initialized module errors. Here's why they happen and the 4 reliable ways to fix them without restructuring your entire project.

Pythoncircular importImportErrormodulesdebugging

What Is a Circular Import?

Circular import happens when module A imports module B, and module B imports module A. Python starts loading A, sees it needs B, starts loading B, sees it needs A — but A isn't finished loading yet. Python gives B a partially-initialized version of A, causing either ImportError or AttributeError when B tries to use something from A that hasn't been defined yet.

Error: ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import)

Or:

Error: ImportError: cannot import name 'process_order' from 'services' (circular import)

The stack trace usually shows two files importing each other. That's the circular pair.

How to Diagnose It

The error message often doesn't name the circular pair directly. Use this to find it:

bash
python -c "import your_module" 2>&1

Or add a print at the top of suspected files:

python
print(f"Loading {__name__}")

When you see the same module name print twice before the error, that's the circular pair.

For Django projects:

bash
python -c "import django; django.setup(); from myapp import models"

Fix 1: Move the Import Inside the Function

Simplest fix. Instead of importing at module level (top of file), import inside the function that actually needs it. The import runs only when the function is called — by then, both modules are fully loaded.

python
# ❌ models.py — module-level import causes circular dependency
from services import send_welcome_email

class User(models.Model):
    email = models.EmailField()

    def save(self, *args, **kwargs):
        is_new = self.pk is None
        super().save(*args, **kwargs)
        if is_new:
            send_welcome_email(self.email)

Fix: Move import inside the method.

python
# ✅ models.py — local import, no circular dependency
class User(models.Model):
    email = models.EmailField()

    def save(self, *args, **kwargs):
        is_new = self.pk is None
        super().save(*args, **kwargs)
        if is_new:
            from services import send_welcome_email  # runs only when called
            send_welcome_email(self.email)

Note: Local imports are slightly slower (Python checks sys.modules each call) but the overhead is negligible. In hot paths called thousands of times per second, cache the import result. For normal application code, local imports are fine.

Fix 2: Use TYPE_CHECKING for Type Hints

If the circular import exists only for type hints (you reference a class in a type annotation but don't actually call it), use the TYPE_CHECKING guard. The import runs only during static analysis, never at runtime.

python
# ❌ services.py imports User just for type hints — circular
from models import User

def create_order(user: User, items: list) -> dict:
    ...

Fix: Guard with TYPE_CHECKING.

python
# ✅ Import never runs at runtime
from __future__ import annotations  # makes all annotations strings at runtime
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models import User  # only runs during type checking, not at runtime

def create_order(user: User, items: list) -> dict:
    ...

from __future__ import annotations makes all type annotations lazy strings — Python doesn't evaluate them at import time. This eliminates most circular imports caused by type hints.

Fix 3: Extract Shared Code to a Third Module

When A and B both need the same thing, and importing from each other creates the circle, extract that shared code into a new module C that neither A nor B imports back.

# ❌ Circular: models.py ↔ services.py
models.py imports from services.py
services.py imports from models.py

# ✅ Extract shared types to base.py
base.py      → no imports from models or services
models.py    → imports from base.py only
services.py  → imports from base.py only

This is the correct architectural fix when local imports feel like a hack. Common extraction targets: constants, base classes, type definitions, utility functions.

Fix 4: Restructure Using Dependency Injection

Instead of modules importing each other, pass dependencies as function arguments or constructor parameters.

python
# ❌ notification.py imports user_service, user_service imports notification
from notification import send_email

def create_user(email):
    user = User(email=email)
    send_email(user)  # notification dependency baked in

Fix: Pass the notification function as a parameter.

python
# ✅ No circular import — caller provides the dependency
def create_user(email, notify_fn=None):
    user = User(email=email)
    if notify_fn:
        notify_fn(user)

# In the caller (where both are imported safely)
from user_service import create_user
from notification import send_email

create_user("alice@example.com", notify_fn=send_email)

When Circular Imports Are a Design Signal

Circular imports often mean two modules are too tightly coupled — they each know too much about each other. The four fixes above resolve the import error, but if you find yourself doing multiple local imports or the circle involves 3+ files, it's worth restructuring.

PatternBetter approach
Model imports serviceModel emits signal/event → service listens
Service imports model imports serviceExtract to shared types.py
Utils imports app codeUtils should have no app dependencies
__init__.py re-exports cause circlesFlatten imports, avoid __init__.py re-exports

For complex multi-file circular imports, paste the import chain into DebugAI. It traces which module imports which across files and identifies the minimal cut point — which import to move or which file to extract.

Debug faster starting today.

Free VS Code extension. 10 sessions/day. No credit card.

Install Free →

Related Posts

Engineering

Codebase-Aware AI Debugging vs Generic AI: Why Context Changes Everything

7 min read

Engineering

How to Debug a React Application in VS Code (Complete Guide)

8 min read

← All posts