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 is not 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 has not 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 is the circular pair.

How to Diagnose It

The error message often does not 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 is 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, 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)

Move the 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
            send_welcome_email(self.email)

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

Fix 2: Use TYPE_CHECKING for Type Hints

If the circular import exists only for type hints, use the TYPE_CHECKING guard. The import runs only during static analysis, never at runtime.

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

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

Guard it with TYPE_CHECKING:

python
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models import User

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

from __future__ import annotations makes all type annotations lazy strings at runtime. Python does not evaluate them at import time, which eliminates most circular imports caused purely 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 the shared code into a new module C that neither A nor B imports back.

# Circular
models.py imports from services.py
services.py imports from models.py

# Fixed
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 workaround. Common extraction targets are constants, base classes, type definitions, and 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)

Pass the notification function as a parameter instead:

python
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. The four fixes above resolve the import error, but if you find yourself adding multiple local imports or the circle involves 3 or more files, it is worth restructuring.

PatternBetter approach
Model imports serviceModel emits signal or 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

GitHub Copilot Just Changed Its Pricing. What Developers Need to Know

5 min read

Engineering

Why Your AI Agent Harness Fails at Debugging (And How to Fix It)

5 min read

← All posts