ImportError: cannot import name 'Order' from partially initialized module
'app.models' (most likely due to a circular import)
The message is accurate and the usual response is not. Somebody moves the import inside the function, the error goes away, and the actual finding — these two modules are not two modules — is lost.
Why it fails, precisely
The loader inserts a module object into sys.modules before executing
its body. That is deliberate: it is what stops an import cycle from
recursing forever. It also means a module can be observed half-built.
Now trace import app.models where models imports services and
services imports models:
-
app.modelsis created, empty, and put insys.modules. -
Its body runs and hits
import app.services. -
app.servicesis created and its body runs. -
It hits
from app.models import Order.app.modelsis insys.modules— so no re-import happens. Python looks up the attributeOrderon the half-built module.Orderhas not been defined yet. -
ImportError.
The FAQ states the consequence exactly: a circular import is fine when both
modules use import module and refer to module.Name later, at call
time. It fails when the second module wants a name out of the first at
module top level, because the first is still mid-execution.
So import app.models followed by app.models.Order inside a function
works. from app.models import Order at the top does not. Same cycle, and
the difference is when the name is looked up.
💡A cycle exists but nothing crashes, and it has been that way for a year. Then somebody reorders two imports and the build breaks. What changed? click to reveal
Nothing about the cycle. What changed is which module the interpreter entered first.
In a two-module cycle, one of them is always the entry point and gets fully
executed; the other observes it half-built. Whether that is a problem depends
on which names the half-built module needs and whether they were defined
above or below the import statement. A cycle can therefore be latent for
years and become fatal because of an alphabetised import block, a new
__init__.py re-export, or a test importing the pair in the other order.
This is why “it works” is not evidence that a cycle is safe. It is evidence that today’s entry order happens to be the working one. The graph is the thing to fix, not the ordering.
The four fixes, in order of preference
1. Restructure. The cycle is information: either these two modules are
really one, or a type they both need belongs in a third module that neither
imports back. Most cycles in real code are a models ↔ services pair where
a couple of shared dataclasses want to live in domain. This is the only
fix that makes the graph better.
2. Invert the dependency with a Protocol. If services needs something
from models and models needs something from services, one of those
directions is wrong. Have the inner module declare a Protocol describing
what it needs, and let the outer module satisfy it structurally. No import
points back, and the checker verifies the contract.
3. if TYPE_CHECKING: for annotation-only imports. If the import exists
purely to write an annotation, put it in a TYPE_CHECKING block and quote
the annotation (or rely on from __future__ import annotations). The name is
never looked up at runtime, so the cycle never forms. This is legitimate and
common — it is what ruff’s TC rules are for.
4. A function-local import, as a last resort. It works: by the time the function runs, both modules are fully initialised. Be honest about what it costs:
-
a dict lookup in
sys.moduleson every call — small, but not free in a hot loop; - it is invisible to import-graph tooling, so your linter’s layering contract stops seeing the edge;
-
it moves
ImportErrorfrom process start to whenever that branch first executes, which may be in production, on the error path, at 3 a.m.
💡Someone suggests PEP 810's lazy import as a general fix for circular imports. Is it?
click to reveal
No. Two limitations make it the wrong tool for this job.
It does not fix cycles in the general case — deferring the binding changes when the partially-initialised module is observed, not whether it can be. A lazy import that is first touched while the other module is still executing fails in exactly the same way.
And it is module-scope only, so it cannot replace the function-local import in the cases where the function-local import is genuinely needed.
Related, since it comes up in the same conversation: PEP 781, which would
make TYPE_CHECKING a builtin so you could drop the from typing import TYPE_CHECKING line, is still Draft and did not land in 3.15. Keep
importing it.
The trap that passes review
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from app.models import Order
def load(order_id: int) -> "Order":
return Order.get(order_id) # NameError at runtime
Order is used as a value, not just an annotation. mypy is perfectly
happy — the name exists as far as the checker is concerned. At runtime the
if was false, Order was never bound, and the first call raises
NameError.
Ruff catches it: TC004, “move import out of type-checking block; used for
more than type hinting”. It is worth enabling the whole TC family for this
one rule alone.
Making the graph visible
You cannot fix what you cannot see. python -X importtime -c "import app"
shows the import tree; import-linter’s forbidden and layers contracts
fail the build on an edge you have declared illegal; ruff analyze graph
emits the dependency graph as JSON.
The problem below is the primitive underneath all of those: find every elementary cycle in an import graph, deterministically. Once you can produce that list, “no cycles” becomes a test rather than a habit.