Fix: Python Circular Import Error — ImportError and Cannot Import Name
Part of: Python Errors
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.
Here is the exact failure mechanism step by step:
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'The “partially initialized” wording in the error message is the key clue. Python did find the module — it is in sys.modules — but the module object is incomplete. Any name defined below the point where the circular import triggered has not been executed yet. That is why the same code sometimes works when you move a function definition above the import statement that creates the cycle.
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 - Type annotation imports — importing a class purely for a type hint creates a runtime dependency that would not exist in a language with separate compilation
Diagnostic Timeline
When you first see cannot import name 'X' from partially initialized module, the reflex is “move the import.” But which import, and where? This timeline walks through the real debugging process.
Minute 0 — Read the traceback carefully. The traceback shows the full import chain. Look at the stack frames from bottom to top:
Traceback (most recent call last):
File "app.py", line 1, in <module>
from routes import router
File "routes.py", line 2, in <module>
from models.user import User
File "models/user.py", line 3, in <module>
from services.auth import hash_password
File "services/auth.py", line 1, in <module>
from models.user import User # ← cycle closes here
ImportError: cannot import name 'User' from partially initialized module 'models.user'The cycle is: app → routes → models.user → services.auth → models.user. The last frame is where the cycle closes.
Minute 2 — Map the dependency direction. Draw the import chain on paper or in a comment. Identify the edge that closes the cycle. In the example above, services.auth importing models.user is the problematic edge — auth needs User only for a type annotation or a runtime call.
Minute 5 — Determine if the import is needed at module level. Ask: does services/auth.py use User at the top level (class body, default argument, decorator), or only inside a function body? If only inside a function, a lazy import solves it immediately.
Minute 8 — Check if the import is only for type hints. Open the file where the cycle closes. If every usage of the imported name is in a type annotation (def hash_password(user: User)) and never called as a runtime value, use TYPE_CHECKING:
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models.user import UserMinute 12 — If the import is needed at runtime, restructure. Extract the shared dependency into a third module. Move hash_password out of services/auth.py into a module that does not import User, or move the shared base class into models/base.py that neither user.py nor auth.py depend on.
Minute 15 — Verify with a clean import. After the fix, run python -c "import app" with the -v flag to confirm no cycle remains:
python -v -c "import app" 2>&1 | grep "models\|services"If the same module name appears twice in the output and the second occurrence is before the first module finishes loading, the cycle still exists.
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'))AttributeError instead of ImportError — in some Python versions (3.7-3.9), a circular import does not raise ImportError. Instead, the partially-loaded module is returned successfully, and you get AttributeError: module 'X' has no attribute 'Y' at the point where the missing name is used. This can happen much later than the import statement, making it harder to trace. Use python -v to check whether the module was imported more than once.
Circular import triggered only under test — pytest imports test files as modules. If a test file imports from a module that transitively imports back to the test file’s package, a cycle forms that does not exist in production. Fix by moving shared test fixtures into conftest.py, which pytest loads before test modules.
importlib.import_module hides cycles from linters — if you use importlib.import_module('mypackage.submod') dynamically, static analysis tools like pydeps and importlab cannot detect the cycle. Add a comment # circular risk: mypackage.submod next to dynamic imports so future readers know to check for cycles manually.
For related Python issues, see Fix: Python ModuleNotFoundError in Virtualenv, Fix: FastAPI Dependency Injection Error, Fix: Python Decorator Not Working, and Fix: Python Logging Not Working.
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.