Fix: Python Circular Import Error — ImportError and Cannot Import Name
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 loadedWhy 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 other —
Usermodel importsPost,Postmodel importsUser __init__.pyre-exporting creates cycles — a package’s__init__.pyimports from submodules that import from the package- Utility modules importing from the app — a helper imports from
appto access config, whileappimports 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.pyAfter (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 dependencyCommon 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+ alternative — from __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 neededFix 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__.pyFix 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 UserServiceFix 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 dbFix 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 mypackageManual 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 occurPython’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 modulesFix 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 appDjango — 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 errorCheck 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 circularsys.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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors
How to fix Python packaging issues — pyproject.toml setup, build backends (setuptools/hatchling/flit), wheel vs sdist, editable installs, package discovery, and twine upload to PyPI.
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.