Python Import Error: Circular Import Detected
A circular import happens when two or more Python modules depend on each other, directly or indirectly, and Python encounters a partially-initialized module while trying to complete the import chain — the fix almost always involves restructuring the dependency, not just reordering import statements.
The Problem
Running your code produces an import error referencing a module that appears to exist and be correctly written:
ImportError: cannot import name 'User' from partially initialized module 'models'
(most likely due to a circular import)
Both modules involved look syntactically correct individually, which makes the error confusing until the actual dependency cycle is understood.
Why It Happens
When module A imports module B, and module B (directly or through a longer chain) imports back from module A, Python can end up trying to access something from module A before it has finished executing — since module A is still in the middle of running its own import of B when B tries to import back from it. Common patterns:
- Two modules directly import from each other at the top level, each needing something the other defines
- A longer chain of imports (A imports B, B imports C, C imports back from A) creates the same fundamental cycle, just spread across more files, making it harder to spot at a glance
- A module imports another purely for type hints, creating an unnecessary runtime dependency cycle for something that's only actually needed during static type checking, not at runtime
- Restructuring code (splitting a large file into smaller modules) accidentally introduces a cycle that didn't exist when everything was in one file
The Fix
The most robust fix is restructuring the code so the circular dependency doesn't exist in the first place. Common approaches:
Move the shared code to a third module that both original modules can import from, breaking the cycle entirely:
# Instead of models.py and services.py importing each other,
# move shared definitions to a new shared.py that both import from
# shared.py -> models.py -> (no back-reference to services.py)
# shared.py -> services.py -> (no back-reference to models.py)
Move the import inside the function that needs it (a "deferred" or "local" import), rather than at the top of the file. This works because by the time the function actually runs, both modules have already finished their initial loading, so the circular reference is no longer a problem at that point in execution:
# models.py
def get_related_service():
from services import SomeService # deferred import, not at top of file
return SomeService()
This is a common pragmatic fix, though it's often considered a workaround rather than a true structural solution — reaching for it repeatedly across a codebase is usually a sign the module boundaries themselves need reconsidering.
If the circular import exists purely for type hints (not needed at runtime), use TYPE_CHECKING to avoid the actual runtime import entirely, while still getting proper type checking support in editors and tools:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from services import SomeService # only imported during type checking, not at runtime
def process(service: "SomeService"):
...
Review whether the two modules genuinely need to depend on each other at all, or whether one is doing too much and should be split so the dependency only flows in one direction — often, a circular import is a signal of an underlying design issue (two things that should probably be one module, or a responsibility that's in the wrong place) rather than just a mechanical import ordering problem.
Still Not Working?
If the cycle is spread across several files and hard to trace manually, use a dependency visualization tool to map out the actual import graph rather than tracing it by hand:
pip install pydeps
pydeps your_package --show-cycles
This generates a visual graph of your project's actual import relationships, making cycles — especially longer ones spanning three or more files — much easier to spot than reading through import statements file by file.