Skip to content

Fix: Python Circular Import Error — ImportError and Cannot Import Name

FixDevs ·

Quick Answer

How to fix Python circular import errors — restructuring modules, lazy imports, TYPE_CHECKING guard, dependency injection, and __init__.py import order issues.

The Error

Python raises an import error when two or more modules import each other:

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

Or:

ImportError: cannot import name 'create_app' from partially initialized module 'app'

Or no error at startup, but an AttributeError at runtime because a module was only partially initialized when imported:

AttributeError: module 'mypackage.models' has no attribute 'User'
# Happens when circular import causes module to be cached before fully loaded

Why This Happens

Python caches modules in sys.modules the moment it starts importing them. If module A imports module B, and module B imports module A while A is still being initialized, Python finds the partially-initialized A in sys.modules and returns it — missing any names defined after the circular import point.

Example — the exact failure mechanism:

1. Python starts importing module_a.py
2. Adds 'module_a' to sys.modules (partially initialized — empty so far)
3. module_a.py runs: `from module_b import SomeClass`
4. Python starts importing module_b.py
5. module_b.py runs: `from module_a import OtherClass`
6. Python finds 'module_a' in sys.modules — but it's empty (step 2)
7. ImportError: cannot import name 'OtherClass' from 'module_a'

Common scenarios:

  • Flask/Django models importing each otherUser model imports Post, Post model imports User
  • __init__.py re-exporting creates cycles — a package’s __init__.py imports from submodules that import from the package
  • Utility modules importing from the app — a helper imports from app to access config, while app imports the helper

Fix 1: Restructure to Break the Cycle

The cleanest fix — reorganize code so the dependency goes only one direction. Circular imports are almost always a sign of poor module organization.

Before (circular):

models/user.py  → imports from  models/post.py
models/post.py  → imports from  models/user.py

After (no cycle) — extract shared types:

# models/base.py — shared base classes and types, imports from nowhere
from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    pass

# models/user.py — only imports from base
from .base import Base

class User(Base):
    __tablename__ = 'users'
    id: int
    # No import from post.py

# models/post.py — imports from base and user
from .base import Base
from .user import User

class Post(Base):
    __tablename__ = 'posts'
    author_id: int
    # Has a relationship to User — one-way dependency

Common restructuring patterns:

Before:               After:
A ←→ B               A → C
                      B → C
                      (C has no deps on A or B)

Move the shared code to a third module (C) that both A and B import.

Fix 2: Use Late/Lazy Imports Inside Functions

Import inside a function body — the import happens when the function is called (after all modules are loaded), not at module load time:

# models/user.py
class User:
    def get_posts(self):
        # Import inside the method — no circular import at module level
        from models.post import Post  # ← Lazy import
        return Post.query.filter_by(author_id=self.id).all()
# services/email.py
def send_welcome_email(user_id: int):
    # Delayed import — app is fully loaded by the time this function is called
    from app import mail  # ← Not imported at module level
    user = User.query.get(user_id)
    mail.send(Message('Welcome!', recipients=[user.email]))

Trade-offs of lazy imports:

  • Hides the dependency — harder to understand at a glance
  • Slight performance cost on first call (negligible in practice)
  • Import errors surface at runtime, not at startup

Use lazy imports as a quick fix for deep-seated circular dependencies that would require significant refactoring to eliminate properly.

Fix 3: Use TYPE_CHECKING Guard for Type Annotations

If the circular import exists only because of type annotations, use TYPE_CHECKING:

# models/post.py
from __future__ import annotations  # Makes all annotations strings (lazy evaluation)
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # This block only runs when a type checker (mypy, pyright) runs
    # It does NOT execute at runtime — no circular import
    from models.user import User

class Post:
    def __init__(self, author: User) -> None:  # Type annotation only
        self.author = author

    def get_author(self) -> User:  # Return type annotation
        from models.user import User  # Still need runtime import for isinstance checks
        return User.query.get(self.author_id)

from __future__ import annotations (Python 3.7+) makes all annotations lazy strings — they’re evaluated only when explicitly requested (e.g., with get_type_hints()). This resolves most annotation-related circular imports without TYPE_CHECKING.

Python 3.10+ alternativefrom __future__ import annotations is the default in Python 3.11+ (PEP 563).

# Python 3.10+ — use X | Y union syntax and built-in generics
from __future__ import annotations  # Still useful for 3.7-3.9 compatibility

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from models.order import Order

class User:
    orders: list[Order]  # Uses string annotation — no runtime import needed

Fix 4: Fix init.py Import Order Issues

A package’s __init__.py that imports from its own submodules is a frequent source of circular imports:

# mypackage/__init__.py — PROBLEMATIC if submodules import from __init__.py
from .models import User
from .services import UserService
from .utils import helper_function
# mypackage/services.py — imports from the package
from mypackage import User  # ← Circular: __init__.py imports services, services imports __init__.py

Fix option 1 — remove the re-exports from __init__.py:

# mypackage/__init__.py — keep it minimal or empty
# Don't re-export everything — let callers import directly

# External code:
from mypackage.models import User  # Direct import — no __init__.py involvement
from mypackage.services import UserService

Fix option 2 — use __all__ with deferred loading:

# mypackage/__init__.py
__all__ = ['User', 'UserService']

def __getattr__(name):
    # Only import when actually accessed — lazy loading
    if name == 'User':
        from .models import User
        return User
    if name == 'UserService':
        from .services import UserService
        return UserService
    raise AttributeError(f'module {__name__!r} has no attribute {name!r}')

Fix option 3 — import order in __init__.py:

If you must have __init__.py imports, ensure the order doesn’t create cycles. Import base modules first, then modules that depend on them:

# mypackage/__init__.py — correct order
from .config import Config    # No deps on other package modules
from .database import db      # Depends only on Config
from .models import User      # Depends on db
from .services import email   # Depends on User and db

Fix 5: Use Dependency Injection to Avoid Imports

Instead of importing from another module at the top level, accept dependencies as function parameters or constructor arguments:

# BEFORE — circular import because auth imports from app
# auth/decorators.py
from app import current_user  # ← Circular: app imports auth, auth imports app

def login_required(f):
    def wrapper(*args, **kwargs):
        if not current_user.is_authenticated:
            return redirect('/login')
        return f(*args, **kwargs)
    return wrapper

# AFTER — accept the dependency as a parameter (dependency injection)
# auth/decorators.py — no imports from app
def login_required(get_current_user):
    """Factory that creates a login_required decorator."""
    def decorator(f):
        def wrapper(*args, **kwargs):
            user = get_current_user()  # Callable passed in — no import needed
            if not user.is_authenticated:
                return redirect('/login')
            return f(*args, **kwargs)
        return wrapper
    return decorator

# app.py — wire it up
from auth.decorators import login_required
from flask_login import current_user

require_login = login_required(lambda: current_user)

# Usage
@app.route('/dashboard')
@require_login
def dashboard():
    return render_template('dashboard.html')

Fix 6: Detect Circular Imports

Finding circular import chains in large projects:

# Install and run pydeps to visualize module dependencies
pip install pydeps
pydeps mypackage --max-bacon=3  # Generates a dependency graph

# Or use importlab
pip install importlab
importlab mypackage

Manual detection — check sys.modules during import:

# Add this temporarily to suspect modules
import sys

class ImportTracer:
    def find_module(self, name, path=None):
        print(f'Importing: {name}')
        return None  # Don't intercept — just log

sys.meta_path.insert(0, ImportTracer())

# Run your app — trace shows the import order and reveals where cycles occur

Python’s --verbose flag:

python -v -c "import mypackage" 2>&1 | grep "import"
# Shows every import in order — cycles appear as re-imports of partially loaded modules

Fix 7: Flask and Django Specific Patterns

Flask — use application factory to avoid circular imports:

# app/__init__.py — application factory pattern
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()  # Create extension without app

def create_app(config=None):
    app = Flask(__name__)

    if config:
        app.config.from_object(config)

    db.init_app(app)   # Attach extension to app

    # Import blueprints here — inside the factory
    # Blueprints aren't imported at module level — no circular import
    from .routes.user import user_bp
    from .routes.post import post_bp
    app.register_blueprint(user_bp)
    app.register_blueprint(post_bp)

    return app

# models/user.py — imports only from extensions, not from app
from app import db  # ← Still circular? Use direct import:

# Better: models/user.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()  # No — this creates a second db instance

# Actually correct:
# models/__init__.py or extensions.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()  # One shared instance

# models/user.py
from . import db  # Import from models package, not from app

Django — use apps.get_model() to avoid importing models at module level:

# WRONG — direct import causes circular import in Django signals
from myapp.models import User

# CORRECT — lazy model reference
from django.apps import apps

def get_user_model():
    return apps.get_model('myapp', 'User')

# In signals.py
from django.db.models.signals import post_save

def user_saved(sender, instance, created, **kwargs):
    if created:
        User = apps.get_model('myapp', 'User')
        # Use User here

post_save.connect(user_saved, sender='myapp.User')

Still Not Working?

Verify the cycle with a minimal reproduction — remove code until only the cycle remains. This identifies exactly which imports cause the issue:

# Minimal test
# a.py
from b import B

# b.py
from a import A  # ← This is the cycle

# Run: python -c "import a"
# Shows the exact error

Check for star imports creating hidden cycles:

# package/__init__.py
from .utils import *   # ← Star import re-exports everything from utils
                       # If utils imports from package, it's now circular

sys.modules inspection at startup:

import sys

# Add after all imports — shows what was imported and in what order
for name, module in sorted(sys.modules.items()):
    if 'mypackage' in name:
        print(name, getattr(module, '__file__', 'built-in'))

For related Python issues, see Fix: Python Logging Not Working and Fix: FastAPI Dependency Injection Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles