Fix: ImportError: cannot import name 'X' from partially initialized module (circular import)
Part of: Python Errors
Quick Answer
How to fix Python's circular import error: ImportError cannot import name from partially initialized module. Covers lazy imports, module restructuring, TYPE_CHECKING, Django, Flask, and more.
The Error
You try to run a Python script or start your application and get this:
Python 3.10+:
Traceback (most recent call last):
File "app.py", line 1, in <module>
from models import User
ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import) (/path/to/models.py)Older Python 3:
Traceback (most recent call last):
File "app.py", line 1, in <module>
from models import User
ImportError: cannot import name 'User' from 'models' (/path/to/models.py)Another common variation:
Traceback (most recent call last):
File "app.py", line 1, in <module>
from utils import helper
ImportError: cannot import name 'helper' from partially initialized module 'utils' (most likely due to a circular import) (/path/to/utils.py)The key phrase is “partially initialized module” and “most likely due to a circular import.” Python is telling you that while it was in the middle of loading one module, that module tried to import something from another module that in turn tried to import from the first one. The first module isn’t fully loaded yet, so the name you’re trying to import doesn’t exist yet.
Why This Happens
A circular import occurs when two or more modules depend on each other, directly or indirectly. Python’s import system works by executing module files top to bottom. When module A tries to import from module B, Python starts executing module B. But if module B also tries to import from module A, Python doesn’t start executing module A again (that would cause infinite recursion). Instead, it returns whatever has been defined in module A so far — which is a partially initialized module. If the name you need hasn’t been defined yet at that point, you get the ImportError.
Here is a minimal example:
# module_a.py
from module_b import function_b
def function_a():
return "A"# module_b.py
from module_a import function_a
def function_b():
return function_a()When you run module_a.py:
- Python starts loading
module_a.py. - The first line is
from module_b import function_b, so Python pausesmodule_aand starts loadingmodule_b.py. - The first line of
module_b.pyisfrom module_a import function_a. Python sees thatmodule_ais already being loaded, so it tries to grabfunction_afrom whatevermodule_ahas defined so far. - But
function_ahasn’t been defined yet (Python only got through line 1 ofmodule_a.pybefore pausing). So you getImportError: cannot import name 'function_a'.
This is not a bug in Python. It is an intentional design choice to prevent infinite import loops. The error is telling you that your module dependency graph has a cycle, and you need to break it.
Circular imports are especially common in larger projects where models reference each other, utility functions depend on application logic, or framework code (like Django models and serializers) naturally creates bidirectional dependencies.
If you are also seeing ModuleNotFoundError alongside this ImportError, that is a separate problem — Python found the module but couldn’t resolve the name, versus Python never finding the module file at all. Solve the ModuleNotFoundError first, then come back to the cycle.
Fix 1: Move the Import Inside the Function (Lazy Import)
The simplest and most common fix. Instead of importing at the top of the file, import inside the function that actually needs it. This delays the import until the function is called, by which time both modules are fully loaded.
Before (broken):
# module_a.py
from module_b import function_b
def function_a():
return function_b()After (fixed):
# module_a.py
def function_a():
from module_b import function_b
return function_b()The import only runs when function_a() is called. By that time, module_b is fully initialized and function_b exists.
This is the go-to fix when you have one or two functions that cause the cycle. It is a common pattern in large Python codebases and is not considered an anti-pattern. Python caches modules after the first import, so the performance cost of importing inside a function is negligible after the first call.
However, if you find yourself adding lazy imports everywhere, it is a sign that your modules are too tightly coupled and you should consider restructuring.
Why this matters: Python doesn’t start loading a module again when it encounters a circular import — that would cause infinite recursion. Instead, it returns whatever has been defined so far in the partially loaded module. If the name you need hasn’t been defined yet at that point, you get
ImportError. Understanding this mechanism is key to choosing the right fix.
Fix 2: Restructure Your Modules
The cleanest long-term solution is to reorganize your code so that the circular dependency no longer exists. Usually this means extracting the shared logic into a third module that both original modules can import from without depending on each other.
Before (circular):
# users.py
from orders import get_user_orders
def get_user(user_id):
...
def get_user_with_orders(user_id):
user = get_user(user_id)
user.orders = get_user_orders(user_id)
return user# orders.py
from users import get_user
def get_order(order_id):
...
def get_user_orders(user_id):
user = get_user(user_id)
...After (restructured):
# users.py
def get_user(user_id):
...# orders.py
def get_order(order_id):
...
def get_user_orders(user_id):
from users import get_user
user = get_user(user_id)
...# user_service.py (new module -- combines both)
from users import get_user
from orders import get_user_orders
def get_user_with_orders(user_id):
user = get_user(user_id)
user.orders = get_user_orders(user_id)
return userThe key idea is that the dependency flows in one direction. user_service depends on both users and orders, but neither users nor orders depends on user_service. This eliminates the cycle.
Fix 3: Use import module Instead of from module import name
When you use import module (instead of from module import name), Python only needs the module object to exist. It does not need the specific name to be defined at import time. The name is looked up later, when you actually use it.
Before (broken):
# module_a.py
from module_b import function_b
def function_a():
return function_b()After (fixed):
# module_a.py
import module_b
def function_a():
return module_b.function_b()This works because import module_b just needs the module_b module object to exist in sys.modules. It does not need function_b to be defined yet. When function_a() is called later, module_b is fully loaded, and module_b.function_b resolves correctly.
This fix does not work in every scenario. If both modules use from X import Y at the top level, you need to change at least one of them. But it is a quick fix that requires minimal code changes.
Fix 4: Create a Shared/Common Module
When two modules depend on each other because they share types, constants, or utility functions, extract those shared pieces into a separate module.
Before (circular):
# auth.py
from user import User, DEFAULT_ROLE
class AuthService:
def login(self, username, password):
user = User(username)
user.role = DEFAULT_ROLE
...# user.py
from auth import AuthService
DEFAULT_ROLE = "viewer"
class User:
def __init__(self, username):
self.username = username
def authenticate(self):
service = AuthService()
...After (shared module):
# constants.py
DEFAULT_ROLE = "viewer"# user.py
from constants import DEFAULT_ROLE
class User:
def __init__(self, username):
self.username = username
self.role = DEFAULT_ROLE
def authenticate(self):
from auth import AuthService # lazy import for remaining dependency
service = AuthService()
...# auth.py
from user import User
from constants import DEFAULT_ROLE
class AuthService:
def login(self, username, password):
user = User(username)
user.role = DEFAULT_ROLE
...By pulling DEFAULT_ROLE into constants.py, you remove one of the reasons auth.py needed to import from user.py. The remaining dependency (User.authenticate needing AuthService) is handled with a lazy import.
Fix 5: Use TYPE_CHECKING for Type Hints
If the circular import only exists because of type annotations, Python provides an elegant solution. The typing.TYPE_CHECKING constant is True only when a type checker (like mypy or pyright) is analyzing the code. At runtime, it is False, so the import never actually executes.
# module_a.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from module_b import ClassB
class ClassA:
def process(self, obj: ClassB) -> None:
...# module_b.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from module_a import ClassA
class ClassB:
def handle(self, obj: ClassA) -> None:
...The from __future__ import annotations line is important. It makes all annotations lazy (they are stored as strings and not evaluated at runtime), which means Python won’t try to resolve ClassB or ClassA when the module loads. Without this line, Python would still try to evaluate the type hint at class definition time and fail.
This pattern is widely used in modern Python codebases and is the recommended approach when circular imports are caused solely by type annotations.
Fix 6: Django Circular Imports (Models, Signals, Serializers)
Django projects frequently run into circular imports because models, serializers, views, and signals naturally reference each other. Django provides built-in tools to handle this.
Models referencing each other
Use a string reference instead of importing the model directly. Django resolves the string to the actual model class at runtime:
# orders/models.py
from django.db import models
class Order(models.Model):
# Instead of: from users.models import User
# Use a string reference:
user = models.ForeignKey("users.User", on_delete=models.CASCADE)The string format is "app_label.ModelName". Django handles the import internally, so you never create a circular dependency.
Signals
Signal handlers often import models from multiple apps. Register them in apps.py using the ready() method:
# orders/apps.py
from django.apps import AppConfig
class OrdersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "orders"
def ready(self):
import orders.signals # noqa: F401# orders/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from orders.models import Order
@receiver(post_save, sender=Order)
def order_created(sender, instance, created, **kwargs):
if created:
# safe to import here -- all apps are loaded
from notifications.models import Notification
Notification.objects.create(order=instance)By the time ready() runs, all Django apps and models are fully loaded, so imports inside signal handlers won’t cause circular import issues.
If you are encountering database-related errors in Django rather than import errors, see Fix: OperationalError: no such table in Django.
Serializers
Django REST Framework serializers often need to reference each other. Use lazy imports or the get_serializer pattern:
# users/serializers.py
from rest_framework import serializers
from users.models import User
class UserSerializer(serializers.ModelSerializer):
orders = serializers.SerializerMethodField()
class Meta:
model = User
fields = ["id", "username", "orders"]
def get_orders(self, obj):
from orders.serializers import OrderSerializer
return OrderSerializer(obj.order_set.all(), many=True).dataThe lazy import inside get_orders avoids the circular dependency because the import happens at serialization time, not at module load time.
Real-world scenario: In a Django project, your
models.pyimports a utility fromservices.py, which imports a model frommodels.py. The fix: use Django’s string-based ForeignKey references ("app.Model") for model relationships and lazy imports inside methods for everything else. Django’sready()hook inapps.pyis your friend for signal registration.
Fix 7: Flask Circular Imports (Application Factory Pattern)
Flask applications commonly hit circular imports when the app object is defined in one module and routes or models are defined in others. The application factory pattern is the standard solution.
Before (broken):
# app.py
from flask import Flask
from routes import bp # circular: routes.py imports app
app = Flask(__name__)
app.register_blueprint(bp)# routes.py
from app import app # circular: app.py imports routes
@app.route("/")
def index():
return "Hello"After (application factory):
# app.py
from flask import Flask
def create_app():
app = Flask(__name__)
from routes import bp
app.register_blueprint(bp)
return app# routes.py
from flask import Blueprint
bp = Blueprint("main", __name__)
@bp.route("/")
def index():
return "Hello"# wsgi.py
from app import create_app
app = create_app()The factory pattern eliminates the circular import because routes.py no longer imports the app object. It defines a Blueprint instead. The create_app() function imports the blueprint inside the function body, after the app is created. This is the pattern recommended by the Flask documentation.
For Flask extensions that need the app object (like Flask-SQLAlchemy), use init_app():
# extensions.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()# app.py
from flask import Flask
from extensions import db
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db.init_app(app)
from routes import bp
app.register_blueprint(bp)
return appFix 8: __init__.py Barrel Import Issues
Large Python packages often use __init__.py to re-export names for convenience. This creates a barrel module pattern that is prone to circular imports.
Problematic structure:
# mypackage/__init__.py
from mypackage.models import User, Order
from mypackage.services import UserService, OrderService# mypackage/services.py
from mypackage.models import User # This actually imports from __init__.py first!
class UserService:
...When Python imports mypackage.services, it first needs to load mypackage/__init__.py. That __init__.py tries to import from mypackage.services, which isn’t loaded yet. Circular.
Fix option 1: Remove barrel imports and import directly
# Instead of:
from mypackage import User
# Use:
from mypackage.models import UserFix option 2: Use lazy imports in __init__.py
# mypackage/__init__.py
def __getattr__(name):
if name == "User":
from mypackage.models import User
return User
if name == "OrderService":
from mypackage.services import OrderService
return OrderService
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")The __getattr__ approach (Python 3.7+) delays the import until the name is actually accessed. This is how several major Python libraries handle their public APIs without running into circular imports.
Fix option 3: Control import order in __init__.py
# mypackage/__init__.py
# Import in dependency order -- models first, then services that depend on models
from mypackage.models import User, Order
from mypackage.services import UserService, OrderServiceMake sure the modules that are depended upon are imported first. And critically, have services.py import directly from mypackage.models (not from mypackage), so it doesn’t trigger the __init__.py again.
Fix 9: Dependency Injection Pattern
Instead of importing a dependency directly, pass it in as a parameter. This completely eliminates the import dependency between the two modules.
Before (circular):
# validator.py
from database import save_record
class Validator:
def validate_and_save(self, data):
if self.is_valid(data):
save_record(data)# database.py
from validator import Validator
def save_record(data):
v = Validator()
v.validate(data)
...After (dependency injection):
# validator.py
class Validator:
def validate_and_save(self, data, save_fn):
if self.is_valid(data):
save_fn(data)# database.py
from validator import Validator
def save_record(data):
v = Validator()
v.validate(data)
...
def process(data):
v = Validator()
v.validate_and_save(data, save_fn=save_record)Now validator.py doesn’t import anything from database.py. The save_fn is passed in at call time. This pattern makes the code more testable too, since you can easily pass a mock function in tests.
For class-based dependency injection:
class Validator:
def __init__(self, save_fn=None):
self.save_fn = save_fn
def validate_and_save(self, data):
if self.is_valid(data):
self.save_fn(data)This approach scales well and is especially useful in larger applications where you want to decouple modules without worrying about import order.
How other tools handle this
Circular imports are a cross-language problem, but the ecosystem each language chose has a real impact on the workarounds you reach for. Knowing what other tools do explains why Python’s “lazy import inside the function” pattern feels acceptable while a Go or TypeScript developer would refactor instead.
Ruby never asks you to write from x import y — require loads the whole file and autoload defers loading until a constant is first referenced. Rails uses Zeitwerk, which lazy-loads every constant by file path. A circular reference in Ruby usually surfaces as a NameError or partially defined class, not an ImportError, and the fix is often “let autoload handle it” rather than restructuring modules.
Node.js (CommonJS) handles circular require() by returning the partial module.exports object that exists so far. You get an object with missing keys instead of an error, and the bug shows up later as TypeError: x is not a function. The lazy-import-inside-a-function pattern works in CJS for the same reason it works in Python — by the time the function runs, the cycle is closed.
Node.js (ESM) uses deferred bindings. Static import statements create live bindings that resolve when accessed, so well-behaved cycles between top-level declarations actually work without throwing. ESM still throws when one side of the cycle tries to use a binding from the other side during the initial top-level evaluation. This is closer to Python’s behavior than CJS is.
Go treats every circular import as a compile error — import cycle not allowed. There is no lazy-import escape hatch. The Go answer is always to break the cycle by introducing a third package or moving the shared type. This is strict, but it forces a clean dependency graph from day one.
TypeScript inherits whatever module system you compile to (CJS, ESM, or AMD), but it adds the extra wrinkle that types and values resolve at different times. import type is the TS analogue of Python’s TYPE_CHECKING block — it gets erased at compile time and never participates in runtime cycles. If your only circular dependency is a type, import type removes it entirely.
Rust has no module-level circular import problem because the compiler builds the whole crate graph at once and resolves names across the entire crate. You can declare modules in any order, and the build fails only on actual unused or unresolved items, not on cyclic file imports.
The takeaway: Python’s “import inside a function” is a feature, not a workaround. It works because Python’s import system caches partial modules and re-enters them on demand — the same machinery that causes the error in the first place. In Go or Rust, the equivalent move is to restructure. In TypeScript, you reach for import type. In Node CJS, you hope the missing binding shows up in tests before it hits production.
Still Not Working?
Indirect circular imports
The cycle might not be between just two modules. Module A imports B, B imports C, and C imports A. These indirect cycles are harder to spot. Use a tool to visualize your import graph:
pip install pydeps
pydeps mypackage --clusterOr trace the imports manually by reading the traceback carefully. Python’s traceback shows the full chain of imports that led to the error.
Import side effects
If a module runs code at import time (like calling functions, creating database connections, or initializing global state), that code might trigger imports that create cycles. Move side effects into functions that are called explicitly, not at import time.
# Bad: runs at import time
db = connect_to_database() # this might import config, which imports this module
# Good: runs when you call it
def get_db():
if not hasattr(get_db, "_connection"):
get_db._connection = connect_to_database()
return get_db._connectionCircular imports that only fail sometimes
If the error only appears when you run a specific script but not others, it depends on which module Python loads first. The order matters because it determines which module is “partially initialized” when the cycle is hit. This doesn’t mean the circular import is harmless in other cases — it’s still a latent bug that can surface when import order changes.
Checking your Python and pip setup
If you’ve fixed the circular import but are still seeing import errors, the problem might be elsewhere. Make sure your packages are installed correctly and that pip can build the packages you need before assuming the cycle is back.
Editable installs and namespace packages
If you installed your own package with pip install -e ., Python loads it from the source tree on every interpreter start. A stale .pth file or a leftover __pycache__ directory from a previous layout can cause the import system to find a different copy of a module than the one you’re editing, which looks identical to a circular import in the traceback. Reinstall with pip install -e . --force-reinstall and delete every __pycache__ under the package before debugging further.
Namespace packages (PEP 420, no __init__.py) are also more sensitive to cycles than regular packages because Python merges multiple directories into a single logical package. If two of those directories cross-import, you can hit a cycle that doesn’t exist in any single source tree.
Running modules as scripts vs imports
python module_a.py and python -m mypackage.module_a set up sys.path and __name__ differently. A module that imports cleanly as part of the package can hit a cycle when you run it as a top-level script because the interpreter binds it to __main__ and then re-imports the same file under its package name. If your error only appears with python file.py and not with python -m, prefer the -m form or move the executable code under if __name__ == "__main__":.
pytest, uvicorn, and other reloaders
Test runners and ASGI servers with --reload rebuild your import graph repeatedly, sometimes in a different order than a normal startup. A latent cycle that runs fine in production can fail in pytest because the collector imports test files before your app’s entry point. Run pytest -p no:cacheprovider --collect-only to see the exact import order pytest uses, and reproduce the failure from a plain python -c "import yourpkg" first.
Debugging the import
Add print statements to see what’s happening during import:
# module_a.py
print(f"module_a: starting import, __name__={__name__}")
from module_b import something
print(f"module_a: finished import")You can also use Python’s verbose import flag to see every import step:
python -v app.pyThis prints a line for every module Python imports, showing you the exact order and where each module is loaded from.
Make sure your code does not have syntax issues like unexpected indentation that could mask the real circular import error with a confusing traceback.
Related: If Django models can’t find their tables after the import is clean, that’s a migration issue rather than a code issue. The same restructuring discipline that broke your import cycle also makes those migrations easier to reason about.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Gunicorn Not Working — Worker Timeout, Boot Errors, and Signal Handling
How to fix Gunicorn errors — WORKER TIMEOUT killed, ImportError cannot import app, worker class not found, connection refused 502 behind nginx, graceful reload not working, and sync vs async worker selection.
Fix: APScheduler Not Working — Jobs Not Running, Gunicorn Duplicates, and Timezone Issues
How to fix APScheduler — BackgroundScheduler exits when script ends, jobs run multiple times under Gunicorn, AsyncIOScheduler not firing, misfire_grace_time skips, and timezone-aware cron triggers.
Fix: Dramatiq Not Working — Actor Not Found, Broker Connection, Retries, and Django Integration
How to fix Dramatiq errors — ActorNotFound on worker, broker connection refused, Redis vs RabbitMQ trade-offs, message retries not triggering, async actors, and django-dramatiq AppConfig setup.
Fix: Marshmallow Not Working — Schema Errors, Load vs Dump, and Field Validation
How to fix Marshmallow errors — Schema not validated on dump, ValidationError messages format, unknown field handling, missing vs default, post_load object construction, and Marshmallow 3 to 4 migration.