Circular Import Mechanics

A circular import is two (or more) modules that import each other: a imports b while b imports a. Whether this works or raises depends entirely on when each module touches the name it wants from the other — and the deciding fact is that CPython inserts a module into sys.modules before running its body. A cyclic re-entry therefore finds a real but partially initialized module object: the names defined after the import statement that triggered the cycle do not exist yet. import a (which binds the module object) almost always survives a cycle because it defers attribute access; from a import name (which immediately binds an attribute) raises ImportError: cannot import name 'name' from partially initialized module 'a' when that attribute has not been defined yet (per the import reference and Python/ceval.c). This note traces the exact mechanism as of CPython 3.14.5 and shows how to read the resulting traceback.

Mental Model: The Module Is Real Before It Is Finished

The single fact that explains every circular-import outcome is this, stated plainly in the import reference: “The module will exist in sys.modules before the loader executes the module code. This is crucial because the module code may (directly or indirectly) import itself; adding it to sys.modules beforehand prevents unbounded recursion in the worst case and multiple loading in the best.” (See sys.modules and the Module Cache for the cache itself.)

So when a’s body runs import b, and b’s body in turn runs import a, that second import a does not re-execute a — it finds a already sitting in sys.modules and returns it immediately. The catch: a is only partially run. Everything textually above the import b line in a has executed; everything below it has not. The names defined below do not exist on the module yet.

sequenceDiagram
    participant Run as interpreter
    participant SM as sys.modules
    participant A as module a (body)
    participant B as module b (body)
    Run->>SM: import a -> insert empty a, mark _initializing=True
    Run->>A: exec a body
    A->>A: (top of a runs)
    A->>SM: import b -> insert empty b, mark _initializing
    A->>B: exec b body
    B->>SM: import a -> ALREADY PRESENT, returns partial a
    Note over B: a.x not defined yet!<br/>import a: OK (binds module)<br/>from a import x: ImportError<br/>a.x access: AttributeError
    B-->>A: b body finishes
    A->>A: x = "a-value"  (rest of a runs)
    A-->>Run: a body finishes

Figure: the timeline of ab. The insight: at the moment b re-enters a, the module object a exists in sys.modules but its body has only run up to the import b line — so a.x, defined below that line, is missing. Whether the cycle survives depends on whether b merely binds the module (import a, fine) or demands a not-yet-defined attribute (from a import x / a.x, fails).

The Mechanism, Step by Step

The insertion-before-execution happens in _load_unlocked in Lib/importlib/_bootstrap.py:

def _load_unlocked(spec):
    module = module_from_spec(spec)
    # This must be done before putting the module in sys.modules
    # (otherwise an optimization shortcut in import.c becomes wrong).
    spec._initializing = True
    try:
        sys.modules[spec.name] = module          # (1) inserted FIRST
        try:
            spec.loader.exec_module(module)      # (2) body runs SECOND
        except:
            try:
                del sys.modules[spec.name]       # (3) on failure, remove it
            except KeyError:
                pass
            raise
        module = sys.modules.pop(spec.name)      # (4) re-fetch + move to end
        sys.modules[spec.name] = module
    finally:
        spec._initializing = False               # (5) clear the flag
    return module

Reading the numbered points: (1) the freshly created, empty module object is placed into sys.modules under its name. (2) only then does exec_module run the module’s body, which is where any import statements inside the module fire — and where a cycle re-enters. (3) if the body raises, the module is removed from sys.modules so a later retry starts clean (this is why a failed import does not leave a broken half-module cached). (4) on success the module is popped and re-inserted to move it to the end of the ordered dict. (5) the _initializing flag — set to True at the top and cleared in the finally — is the marker the rest of the machinery uses to recognize “this module is mid-execution,” i.e. partial.

That _initializing flag is consulted in _find_and_load, the function __import__ ultimately calls:

def _find_and_load(name, import_):
    module = sys.modules.get(name, _NEEDS_LOADING)
    if (module is _NEEDS_LOADING or
        getattr(getattr(module, "__spec__", None), "_initializing", False)):
        with _ModuleLockManager(name):
            module = sys.modules.get(name, _NEEDS_LOADING)
            if module is _NEEDS_LOADING:
                return _find_and_load_unlocked(name, import_)
        _lock_unlock_module(name)
    # ...
    return module

When b runs import a, this checks sys.modules. a is present (the _NEEDS_LOADING sentinel is not returned), so _find_and_load does not re-run a’s body. It simply returns the existing — partial — module object. That return value is exactly what gets bound to the name a in b’s namespace. This is why import a survives a cycle: it binds the module object, which already exists; it never asks whether any particular attribute of a is defined yet.

Why import a Survives but from a import x Raises

The asymmetry is not “import good, from-import bad” — it is about when the attribute is accessed. import a binds the module object and defers any attribute access to later. from a import x demands the attribute x right now, at import time, while a is still partial.

from a import x compiles to an IMPORT_NAME (which runs __import__ and yields the module) followed by IMPORT_FROM x. The IMPORT_FROM opcode is handled by _PyEval_ImportFrom in Python/ceval.c:

PyObject *
_PyEval_ImportFrom(PyThreadState *tstate, PyObject *v, PyObject *name)
{
    PyObject *x;
    if (PyObject_GetOptionalAttr(v, name, &x) != 0) {
        return x;          // attribute exists -> success
    }
    /* Issue #17636: in case this failed because of a circular relative
       import, try to fallback on reading the module directly from
       sys.modules. */
    /* ... build fullmodname = "a.name", try PyImport_GetModule(...) ... */
    /* if still nothing: */
 error:
    /* ... construct the ImportError message ... */
}

Step by step: IMPORT_FROM first does getattr(a, "x"). If x is an attribute of a, it returns it — done. If not (the partial-module case, where x is defined below the line that triggered the cycle), it falls back to checking whether a.x is itself an importable submodule in sys.modules (this handles the legitimate case from package import submodule during a relative-import cycle, bpo-17636). If that also fails, it constructs an ImportError. The message is chosen by inspecting the spec’s _initializing flag:

int rc = _PyModuleSpec_IsInitializing(spec);
if (rc > 0) {
    /* ... */
    errmsg = PyUnicode_FromFormat(
        "cannot import name %R from partially initialized module %R "
        "(most likely due to a circular import) (%S)",
        name, mod_name_or_unknown, origin);
}

So the famous “cannot import name ‘x’ from partially initialized module ‘a’ (most likely due to a circular import)” message is emitted precisely when the attribute is missing and the source module’s spec is still _initializing. If the module had already finished (rc == 0) but the attribute were genuinely absent, you would instead get the plainer "cannot import name 'x' from 'a'" — a real typo or a name that never existed.

By contrast, import a (without from) never reaches IMPORT_FROM; it just binds the module object. The cost is deferred: if b then uses a.x at its own top level (still during the cycle), the access fails — but with AttributeError, not ImportError, because now it is an ordinary attribute lookup on a real module object. Both are the same underlying problem (the attribute does not exist yet); they differ only in which opcode hits it first.

Three reproducible outcomes (verified on 3.14.5)

With a.py = import b then x = "a-value", and varying b.py:

b.pyResultWhy
from a import xImportError “cannot import name ‘x’ from partially initialized module ‘a’”IMPORT_FROM demands a.x while a is partial
import a
print(a.x)
AttributeError “module ‘a’ has no attribute ‘x’”module binds fine; a.x access fails because x not defined yet
import a
def get(): return a.x
worksa.x is only read when get() is called, long after a finished

The third row is the whole game: deferring the attribute access until after the cycle has fully unwound makes the partial-module window irrelevant.

Resolved (2026-06-01)

Verified against Python/ceval.c at the v3.14.5 tag (the IMPORT_FROM error path, lines ~3214–3275) and reproduced on live 3.14.5. The branch key is int is_possibly_shadowing = _PyModule_IsPossiblyShadowing(origin) (line 3214), and there are actually three message forms, not two:

  1. Stdlib shadowing (is_possibly_shadowing_stdlib): “cannot import name … from … (consider renaming … since it has the same name as the standard library module named … and prevents importing that standard library module)”.
  2. Non-stdlib shadowing while the module is initializing: “cannot import name … from … (consider renaming … if it has the same name as a library you intended to import)” — this is the form quoted above; live reproduction with a top-level a.pyb.py cycle gave exactly cannot import name 'y' from 'b' (consider renaming '/…/b.py' if it has the same name as a library you intended to import).
  3. Plain circular import (no shadowing): “cannot import name … from partially initialized module … (most likely due to a circular import) (…)” — reproduced with a mypkg.amypkg.b package cycle.

The takeaway for a reader debugging a cycle is unchanged: any of these phrasings means the attribute is not defined yet. Source: ceval.c@v3.14.5.

The Fix: Move or Defer the Import

Two fixes follow directly from the mechanism.

Function-local imports. Moving from a import x from the top of b into the function that needs it defers the import until call time, by which point a has finished initializing and a.x exists:

# b.py — top-level import would fail mid-cycle; function-local does not
def do_work():
    from a import x      # runs only when do_work() is called, after a finished
    return x

This is the canonical fix and is essentially free (a local import is a cheap sys.modules lookup after the first time, see sys.modules and the Module Cache).

Bind the module, access attributes lazily. Replacing from a import x with import a and using a.x at the point of use (inside functions, not at module top level) works for the same reason — the module binding succeeds during the cycle and the attribute is only read later.

A third, structural fix is to break the cycle: extract the shared names a and b both depend on into a third module c that neither imports back, so the dependency graph becomes acyclic. This is the right fix when the cycle reflects a genuine design problem rather than an ordering accident.

Thread Safety: Per-Module Locks and Deadlock Avoidance

Circular imports are dangerous in threaded code for a second reason beyond partial modules: two threads importing each other’s modules could deadlock. CPython guards each module with its own _ModuleLock (a re-entrant lock) rather than one global import lock, and it actively detects import deadlocks. From Lib/importlib/_bootstrap.py:

class _ModuleLock:
    """A recursive lock implementation which is able to detect deadlocks
    (e.g. thread 1 trying to take locks A then B, and thread 2 trying to
    take locks B then A)."""
 
    def acquire(self):
        tid = _thread.get_ident()
        with _BlockingOnManager(tid, self):
            while True:
                with self.lock:
                    if self.count == [] or self.owner == tid:
                        # unowned, or owned by THIS thread -> grant immediately
                        self.owner = tid
                        self.count.append(True)
                        return True
                    if self.has_deadlock():
                        raise _DeadlockError(f'deadlock detected by {self!r}')
                    # else queue up as a waiter and block on self.wakeup

Two design choices matter for circular imports. First, the lock is re-entrant (RLock-like): if the same thread that already holds module a’s lock re-enters to import a again (exactly what a single-threaded circular import does), the self.owner == tid branch grants the lock immediately — “This supports circular imports (thread T imports module A which imports module B which imports module A),” per the source comment. Without re-entrancy, a single-threaded cycle would self-deadlock. The count is tracked as a list of True because list.append/list.pop are atomic in CPython.

Second, when a different thread holds the lock, has_deadlock() walks the global _blocking_on thread→lock graph (via _has_deadlocked) to see whether granting would close a cycle (thread 1 holds A wants B; thread 2 holds B wants A). If so it raises _DeadlockError rather than hanging forever. The source comment notes this is “more than just a hypothetical” and links to a real Django REST Framework issue. There is also a _DummyModuleLock for interpreters built without threads, and the whole scheme is re-entrancy-aware to survive the import system being re-entered by signal handlers and the garbage collector (a __del__ that imports). This matters more under Free-Threaded CPython, where genuinely concurrent imports are common; see also The Global Interpreter Lock.

Diagnosing a Circular Import from the Traceback

The traceback is a precise map of the cycle. Reading the verified 3.14.5 example for the package cycle mypkg.amypkg.b:

  File ".../mypkg/a.py", line 1, in <module>
    import mypkg.b
  File ".../mypkg/b.py", line 1, in <module>
    from mypkg.a import x
ImportError: cannot import name 'x' from partially initialized module 'mypkg.a'
             (most likely due to a circular import) (.../mypkg/a.py)

Three signals to read: (1) the phrase “partially initialized module” plus “(most likely due to a circular import)” is CPython telling you outright this is a cycle, not a typo. (2) The traceback frames show the entry order of the cycle — a was imported first (so it is the one left partial), then it imported b, then b tried to reach back into a. The module named in the error (mypkg.a) is the partial one; the line in b that triggered it is the second frame. (3) The parenthesized path (.../mypkg/a.py) is the origin of the partial module, useful when two modules share a short name.

Compare this against a plain "cannot import name 'x' from 'a'" without “partially initialized” — that means a finished loading and x genuinely is not there (a typo, a removed symbol, a wrong module). And an AttributeError: module 'a' has no attribute 'x' deep in some function, when the rest of the program runs fine, often means a circular import where you used import a; a.x and the access happened to fire during the partial window. The fix in every case follows from the section above: defer the access, or break the cycle.

A useful runtime probe: inside a suspected partial module, import sys; print(sys.modules['a'].__spec__._initializing) is True while the cycle is live. And vars(sys.modules['a']).keys() shows exactly which names have been defined so far — everything below the triggering import line will be absent.

Common Misunderstandings

  • “Circular imports are always an error.” False — they frequently work. The cycle only fails if a not-yet-defined name is demanded during the partial window. Large codebases routinely rely on cycles working, typically via function-local imports or TYPE_CHECKING-guarded imports that never execute at runtime.
  • from a import x is the problem.” The problem is eager attribute access at import time. from a import x is the most common way to do that, but import a followed by top-level a.x fails identically (as AttributeError).
  • sys.modules caches the finished module.” It caches the module object from the moment of creation — including while it is still partial. That is the entire reason cycles can return a half-built module rather than recursing forever.
  • “A failed import leaves a half-module cached.” No — _load_unlocked deletes the module from sys.modules if the body raises, so a retry starts fresh. (A from a import x that fails inside a still-running a does not corrupt a’s eventual completion, though it does abort b.)

See Also