TL;DR
- All 51 popular Python repos I checked (1,000+ GitHub stars each) have circular import chains between files; in the median one, 16.4% of production files sit inside one.
- A Python circular import only fails with "partially initialized module" when a name is read from a module that has not finished running, such as a module-level
from x import y, a base class, or a decorator that runs on import. - Django has a 179-file import cycle and imports fine, because most of its cycles go through imports inside functions or imports used only for type hints.
- Fixes, in order: move the shared code to a third module, import the module instead of the name (with
from __future__ import annotations), put type-only imports underTYPE_CHECKING, or import inside the function.
This is the error that sends people searching:
ImportError: cannot import name 'User' from partially initialized module 'shop.models' (most likely due to a circular import)
The usual advice is "you have a circular import, remove it". But I used repowise to look at the import graphs of 51 popular Python repositories (1,000+ GitHub stars each), and every single one has circular import chains between its files. In the median repo, 16.4% of production files sit inside one. In Django, one chain links 179 files, and Django imports fine.
So a cycle on its own is not the problem. Below: which cycles actually raise that error, why most do not, and how to fix the ones that do.
Reproduce it
Two modules in a package, each importing a name from the other:
# shop/models.py
from shop.services import send_welcome
class User:
def __init__(self, email):
self.email = email
# shop/services.py
from shop.models import User
def send_welcome(user: User):
print(f"welcome, {user.email}")
# main.py
from shop.models import User
from shop.services import send_welcome
send_welcome(User("a@example.com"))
$ python main.py
Traceback (most recent call last):
File "D:\devto-scratch\repro\shop\main.py", line 1, in <module>
from shop.models import User
File "D:\devto-scratch\repro\shop\shop\models.py", line 1, in <module>
from shop.services import send_welcome
File "D:\devto-scratch\repro\shop\shop\services.py", line 1, in <module>
from shop.models import User
ImportError: cannot import name 'User' from partially initialized module 'shop.models' (most likely due to a circular import) (D:\devto-scratch\repro\shop\shop\models.py)
What happens, step by step:
-
main.pyimportsshop.models. Python creates an empty module object, puts it insys.modules, and starts runningmodels.pyfrom the top. - Line 1 of
models.pyimportsshop.services, so Python pausesmodels.pyand starts runningservices.py. - Line 1 of
services.pyasks forUserfromshop.models. The module exists insys.modules, but it is still on line 1.Userhas not been defined yet.
"Partially initialized" means exactly that: the module object exists, its code has not finished running. The cycle is only fatal because a name is read from the half-built module while it is still half built.
How common are circular imports in Python projects?
The 51 Python repos in the set include django, fastapi, airflow, langchain, prefect and paperless-ngx. Looking at file-to-file imports in production code only (tests, generated and vendored code excluded):
- 51 of 51 have at least one circular import group (a set of 2+ files that can all reach each other through imports).
- In the median Python repo, 16.4% of production files sit inside one.
- Across all 195 repos in the wider set, covering every language, the median is 9.2%.
To see why they do not break, I took a fresh clone of Django (commit 6a8248f) and looked at its import graph. Counting every import between the 900 non-test .py files under django/, 238 files sit inside a circular group and the largest group holds 179 files (the whole-repo index counts more, because it also includes files outside django/), from django/__init__.py through apps, conf, db and parts of contrib.
Then I dropped two kinds of import edges: imports written inside a function, and imports used only for type hints. Those two kinds do not run when the module is first loaded. The result:
Django, files under django/
|
Circular groups | Files inside a group | Largest group |
|---|---|---|---|
| All imports | 14 | 238 | 179 files |
| Only imports that run at module load | 8 | 58 | 35 files |
Removing 152 import edges, about 5% of the total, shrinks the files involved by three quarters. Most of Django's cycles exist only because some function, somewhere, imports something when it is called. By then every module has finished loading, so there is nothing half built to trip over. Django's code is explicit about it. django/db/models/query_utils.py has this inside Q.check():
# Avoid circular imports.
from django.db.models import Value
from django.db.models.sql import Query
from django.db.models.sql.constants import SINGLE
git grep -i "circular import" -- 'django/*.py' finds 16 comments like that in 11 files.
The 35-file group that remains is django/db/models, and it survives import because of ordering. django/db/models/__init__.py re-exports names from its submodules, and the order of those imports is pinned:
# Imports that would create circular imports if sorted
from django.db.models.base import DEFERRED, Model # isort:skip
from django.db.models.fields.related import ( # isort:skip
ForeignKey,
ForeignObject,
OneToOneField,
So the real picture of a big Python codebase is: lots of cycles, nearly all of them harmless, and a handful held together by import order. The error shows up when someone reorders those imports or adds a new module-level import in the wrong direction.
Which circular imports raise "partially initialized module"?
1. A module-level from x import y that runs while x is half built
That is the reproduction above. from module import name reads the name immediately. If module is still running higher up the stack, the name does not exist yet.
2. Inheriting from a class across the cycle
A class statement needs its base class at the moment the class is created, so even "import the module, not the name" does not save you:
# plugins/base.py
from plugins.email import EmailNotifier
class Notifier:
def send(self, msg):
raise NotImplementedError
DEFAULT = EmailNotifier
# plugins/email.py
import plugins.base as base
class EmailNotifier(base.Notifier):
def send(self, msg):
print("email:", msg)
File "D:\devto-scratch\repro\inherit\plugins\email.py", line 4, in <module>
class EmailNotifier(base.Notifier):
^^^^^^^^^^^^^
AttributeError: partially initialized module 'plugins.base' from 'D:\devto-scratch\repro\inherit\plugins\base.py' has no attribute 'Notifier' (most likely due to a circular import)
3. Registries and decorators that run on import
The classic web-app layout: app.py imports the views so the routes get registered, and views.py imports app to use its decorator.
# web/app.py
from web import views # load the routes
class App:
def __init__(self):
self.routes = {}
def route(self, path):
def register(fn):
self.routes[path] = fn
return fn
return register
app = App()
# web/views.py
from web.app import app
@app.route("/")
def index():
return "hello"
File "D:\devto-scratch\repro\registry\web\views.py", line 1, in <module>
from web.app import app
ImportError: cannot import name 'app' from partially initialized module 'web.app' (most likely due to a circular import) (D:\devto-scratch\repro\registry\web\app.py)
The decorator runs at import time, so it needs app before app.py has created it.
How do you fix a circular import in Python?
All of these were run on Python 3.13.
Move the shared piece to a third module
The best fix removes the cycle. In the inheritance example, base.py only imported EmailNotifier to set a default. Move the default somewhere that sits above both:
# plugins/base.py
class Notifier:
def send(self, msg):
raise NotImplementedError
# plugins/defaults.py
from plugins.email import EmailNotifier
DEFAULT = EmailNotifier
$ python main.py
email: hi
Now base imports nothing, email imports base, defaults imports email. The arrows only point one way. For the registry example the same idea is an app factory, or a main.py that imports app first and views second.
Import the module, not the name, and defer annotations
from shop import models binds the module object, which exists even while it is half built. You then read models.User later, when it exists. One catch: annotations are evaluated when the function is defined, so this still fails on Python 3.13:
# shop/services.py
from shop import models
def send_welcome(user: models.User):
print(f"welcome, {user.email}")
def send_welcome(user: models.User):
^^^^^^^^^^^
AttributeError: partially initialized module 'shop.models' from 'D:\devto-scratch\repro\shop\shop\models.py' has no attribute 'User' (most likely due to a circular import)
Add from __future__ import annotations at the top of services.py and it runs, because annotations become strings that are never evaluated at import time. (Python 3.14 defers annotation evaluation by default, PEP 649.)
If the import is only for type hints, use TYPE_CHECKING
# shop/services.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from shop.models import User
def send_welcome(user: User):
print(f"welcome, {user.email}")
$ python main.py
welcome, a@example.com
The import only exists for mypy, pyright and your editor. At runtime it never executes.
Import inside the function
# shop/models.py
class User:
def __init__(self, email):
self.email = email
def welcome(self):
from shop.services import send_welcome
send_welcome(self)
This is what Django does in those 16 commented places. It works, and it is fine as a local patch. It is also how a codebase ends up with a 179-file cycle that nobody can see, so leave a comment, like Django does, saying why the import is there.
How do you find circular imports in your own project?
For a list in the terminal, pylint has a check for exactly this:
pylint --disable=all --enable=cyclic-import django/db
On Django's django/db it reported 72 cyclic-import messages in 8.5 seconds, for example:
django\db\models\sql\__init__.py:1:0: R0401: Cyclic import (django.db.models.manager -> django.db.models.query) (cyclic-import)
django\db\models\sql\__init__.py:1:0: R0401: Cyclic import (django.db.models.base -> django.db.models.fields.related) (cyclic-import)
pylint counts imports inside functions too, so expect it to list cycles that never fail at runtime. pydeps yourpackage --show-cycles draws the same cycles as a graph (it needs Graphviz installed). To stop new cycles in CI, import-linter can fail the build on cycles between subpackages. The setup for that, plus the TypeScript and Go equivalents, is in this guide to finding circular dependencies.
To look at the cycles on a picture instead of a list, Django's architecture view shows the same import graph, and the trace tool on that page finds the path between any two files.
How I got these numbers
The 51 repos are the Python part of a set of 195 of the most-starred public repositories indexed on repowise (1,000+ GitHub stars each, 253,688 production source files, November 2026), each measured at its own latest index between May and November 2026. The cross-language numbers are in finding 6 of the code health 2026 report. Cycles are counted on file-to-file imports between production files, so __init__.py re-exports count as edges. The Django breakdown comes from repowise init --yes --no-prose --no-editor-setup on a fresh clone at 6a8248f, then a strongly connected components pass over the stored import graph, once with every import edge and once without the edges marked as function-level or type-only.
FAQ
What does "partially initialized module ... most likely due to a circular import" mean?
Python has created the module object and put it in sys.modules, but the module's code is still running further up the import chain. Something asked for a name that the module has not defined yet, so the import fails with that message.
Does from __future__ import annotations fix circular imports?
Only when the cycle breaks on a type hint. It turns annotations into strings that are not evaluated at import time, so def f(user: models.User) stops failing. It does not help when the name is needed while the module loads, such as a base class or a decorator.
Are circular imports always a bug in Python?
No. All 51 Python repos in this set have circular import chains between files and import fine. A cycle only fails when one module reads a name from another before that module has finished running.
Top comments (0)