← Debugging Guides

阅读中文版本

Python — ImportError: cannot import name from partially initialized module

Resolve ImportError from partially initialized modules by reading the import chain in the traceback, then break Python circular imports with leaf modules or deferred imports.

intermediatedebuggingpythonimportsmodules

Symptoms

  • ImportError: cannot import name 'User' from partially initialized module 'app.models' (most likely due to a circular import)
  • The failure occurs at interpreter startup before any request is served
  • Import order matters: importing module B first works, importing module A first raises the error

Possible causes

  • app.models imports app.schemas, and app.schemas imports classes back from app.models
  • Type annotations reference classes across two modules that both import at module scope
  • A package __init__.py eagerly imports submodules that import each other
  • A runtime import was moved to module top-level during refactoring, closing a loop

Diagnosis steps

Read the import chain in the traceback

python -c "import app.main" 2>&1 | grep "import\|File" | head -20

Interpretation

The traceback lists every import in the loop; the module named twice is the cycle, and the line where it fails shows which name was requested too early.

Reproduce the order sensitivity

python -c "import app.schemas" && python -c "import app.models"

Interpretation

If one entry order works and the reverse raises the partially initialized error, the cycle is confirmed and its direction is known.

Map the cycle mechanically

python -m pip install pydeps && pydeps app --graph-output cycle.svg

Interpretation

A rendered import graph makes the loop visible and shows which edge is cheapest to remove.

Root cause

Python executes a module body on first import and registers it in sys.modules before that body finishes. In a cycle, the partially executed module is already in sys.modules, so the backward import succeeds as an object but the requested name does not exist yet on it, producing the partially initialized ImportError.

Fix

Break the cycle structurally: move shared names into a leaf module both sides import, defer the backward import into the function that needs it, or restructure the two modules into one. For annotations, use from __future__ import annotations plus TYPE_CHECKING imports so type references do not execute at runtime.

Risk note

Blanket try/except around imports hides the cycle and produces NoneType attribute errors later. Fix the graph, not the symptom.

Verify the fix

Import every entry point (server, CLI, jobs) in a clean interpreter and assert zero ImportError; run the import graph check in CI so reintroducing a cycle fails the build.

Symptom walkthrough

Modern Python names the crime in the message itself:

ImportError: cannot import name 'User' from partially initialized
module 'app.models' (most likely due to a circular import)
(most likely due to a circular import)

Read the traceback bottom-up as an import chain: each “File … line … import” frame is one hop. The module appearing twice is the cycle node. The decisive experiment is order sensitivity — import one side first and everything works; import the other side first and the error fires. That asymmetry exists because sys.modules gets the entry before its body finishes, and only the second traversal finds the name missing.

Fix in practice

Pick the cheapest edge to cut. Extracting a leaf is usually cheapest:

app/models/base.py   <- leaf: shared classes and constants
app/models/user.py   <- imports base
app/schemas.py        <- imports base

Defer the backward import when only one function needs it:

# app/schemas.py
def render_user(user):
    from app.models import User   # resolved at call time
    return User.__name__ + ':' + user.id

For annotation-only references, keep them out of runtime entirely:

# app/schemas.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from app.models import User

def serialize(u: 'User') -> dict: ...

Never wrap the failing import in try/except ImportError — it converts a structural error into a delayed AttributeError on None, which is strictly harder to debug.

Verification

  Import every entry point in a clean interpreter: the web server module, the CLI module, the job runner. All must import with zero ImportError regardless of order — run each twice with reversed flags to be sure. Then make the property durable: a CI step that renders the import graph with pydeps and fails when any cycle is detected, so a refactor that closes a new loop fails before review rather than at the next cold start.