skip to content

Circular Imports

Why two modules that import each other sometimes work and sometimes raise ImportError. The answer is half mechanism — a half-built module sits in sys.modules — and half wrong dependency direction.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does Python's ImportError 'cannot import name X from partially initialized module' mean?

level: juniorimportance: must knowfreq 58%

answer

  1. Two files that import each other
  2. The first module has not finished running
  3. A half-built object is already cached
  4. Attribute asked for before it is defined
  5. Module bodies bind names top to bottom

basics

~10 s

Two modules import each other. Python began running the first one, cached it half-finished, and the second one asked it for a name that has not been defined yet, so the name lookup fails.

solid answer

~50 s

Python inserts a module object into `sys.modules` **before** it runs the module's body, so an import of a module already being imported returns a half-built object instead of recursing forever. In a cycle, module `a` starts running, reaches `import b`, and `b`'s body then does `from a import alpha`. The import machinery finds `a` in `sys.modules`, skips re-execution, and tries to read the attribute `alpha` off it — but `a` is still parked on its first line, so `alpha` does not exist and you get `ImportError: cannot import name 'alpha' from partially initialized module 'a' (most likely due to a circular import)`. The module named in the message is the one imported **first**; the module that raises is the one running **second**. The real fix is usually to move the shared name into a third module that neither imports back.

code

python · 11 lines
python
import sys, tempfile, pathlib

d = pathlib.Path(tempfile.mkdtemp())
(d / "planner.py").write_text("from solver import solve\ndef plan():\n    return solve()\n")
(d / "solver.py").write_text("from planner import plan\ndef solve():\n    return 'route'\n")
sys.path.insert(0, str(d))

try:
    import planner
except ImportError as exc:
    print(type(exc).__name__, exc)

go deeper

for a junior

Be able to read the message and say what it means: two files import each other and one asked the other for a name too early. Know the first-aid fix of importing the module rather than the name.

for a middle

Explain the mechanics: the module object goes into the cache before its body runs, and a from ... import name during that window is an attribute read that can miss. Contrast the ImportError and AttributeError forms.

for a senior

Show that you treat the cycle as a design signal, not a syntax puzzle. Say which of the three fixes you would apply and why, and note that a passing import today can break tomorrow when the entry order changes.

for a principal

Own the position that cycles between packages are an architectural defect the codebase should be prevented from reintroducing, and be able to weigh a quick deferred import against paying down the direction properly.

### The message, in full On CPython 3.14 the failure reads: ``` ImportError: cannot import name 'plan' from partially initialized module 'planner' (most likely due to a circular import) (/app/planner.py) ``` The *(most likely due to a circular import)* hint was added in 3.8; before that the message was just `cannot import name 'plan'`, which is why older answers on the subject sound more mysterious than the failure actually is. ### Why a half-built module exists at all Importing a module runs these steps: 1. The import system looks the module name up in `sys.modules`. On a hit it returns the cached object immediately and executes nothing. 2. On a miss, a loader creates an **empty module object** and puts it into `sys.modules` **before executing the body**. That ordering is the whole story. It is deliberate: it is what stops `a` importing `b` importing `a` from recursing forever. 3. The body then runs top to bottom. Each `def`, `class` and assignment binds a name into the module's namespace *at the moment the statement is reached* — a module body is ordinary code, not a declaration block. 4. If the body hits an `import` of its own, steps 1–3 run again, nested inside step 3 of the outer module. So in a cycle there is always a window in which a module is present in the cache but only partly populated. `from a import alpha` is, in effect, `import a` followed by `getattr(sys.modules['a'], 'alpha')`, and inside that window the attribute may simply not be there yet. ### Which module is which The module named in the message is the one whose import started **first** and is still unfinished. The traceback's last frame is in the module that ran **second** and asked for the missing name. Reading the traceback top down gives you the chain: entry point → first module → second module → the failing line. ### The three shapes of the same problem * `from a import alpha` at module level → `ImportError` at import time, with the message above. * `import a` at module level, then `a.alpha` used at module level → `AttributeError: partially initialized module 'a' has no attribute 'alpha' (most likely due to a circular import)`. * `import a` at module level, then `a.alpha` used only **inside a function body** → no error at all. By the time anything calls that function, `a` has finished and the attribute exists. That third case is the single most useful thing to remember: the cycle is not fatal, the *eager attribute read during the cycle* is. ### Why it works until it doesn't Whether the cycle blows up depends on where the needed name sits relative to the import that starts the cycle. If `alpha` is defined **above** `import b` in `a.py`, the attribute already exists when `b` asks for it and everything works — fragile, but working. Move the import up, add a new import, or import the two modules in the other order, and the same code fails. This is why circular imports classically surface as "works when the app starts, fails in one test that imports the other module directly". ### Fixes, best first 1. **Move the shared thing to a third module** both sides import. A cycle is normally a symptom of the dependency direction being wrong, and that direction is a design question in its own right. 2. **Import the module, not the name**: `import a` (or `from package import a`) and touch `a.alpha` at call time. Binding a module object to a name is safe even while that object is half-built, because the same object is filled in place. 3. **Defer the import into the function** that needs it. It works and is cheap after the first call (a `sys.modules` dict hit), but it hides a real dependency from readers and tools, and moves the failure from startup to first use. 4. **`if TYPE_CHECKING:`** when the only reason for the import is an annotation. 5. Moving the import to the bottom of the file works and is the worst option — formatters, linters and the next reader will all fight it. ### What not to do Do not `importlib.reload` your way out, do not delete `sys.modules` entries by hand, and do not wrap the import in `try`/`except ImportError` with a fallback. Each of those can leave two distinct module objects — and therefore two distinct copies of the same class — alive at once, after which `isinstance` checks start failing in ways far harder to debug than the original error. Note also that when the import does fail, CPython removes **both** modules from `sys.modules` again, so simply retrying the import reproduces the identical error rather than picking up where it left off.

  • Does moving the failing import to the bottom of the file fix it?
    Usually yes, and it is the worst fix available. It only works because the names above it are bound by the time the cycle is entered, so any later edit that reorders the file, or an import from a different entry point, breaks it again. Formatters and import-sorting linters will also move it back. Prefer extracting the shared name into a third module.
  • What is the AttributeError version of this failure?
    If you write `import a` and then use `a.alpha` at module level rather than inside a function, the import itself succeeds — you get a valid, half-built module object — and the attribute read fails instead: `AttributeError: partially initialized module 'a' has no attribute 'alpha' (most likely due to a circular import)`. It is the same window, caught one step later.
  • After the ImportError, is the half-built module still in sys.modules?
    No. When a module body raises, the import machinery removes that module from `sys.modules`, and the exception propagates outward removing the enclosing one too. So catching the ImportError and importing again re-runs both bodies from scratch and reproduces the same error — a retry loop never converges.

It is like phoning a colleague who is halfway through writing a report and asking for the conclusion. They picked up — the module is in the cache — but the section you want has not been typed yet.

saying these in an interview costs you the question

  • Claims Python simply cannot handle two modules referencing each other
  • Blames a missing installed distribution or a wrong sys.path
  • Says deleting cached bytecode or reinstalling fixes it
  • Thinks the module named in the message is the one that failed to load
  • Suggests catching ImportError and retrying the import
  • Believes module bodies are declarations, so order does not matter

context

open as a page

In a circular import, why does `import mod` survive where `from mod import name` fails?

level: middleimportance: must knowfreq 48%

basics

~20 s

import mod binds the module object, which is already in the cache and gets filled in place, so a half-built module is fine. from mod import name copies an attribute value immediately, and during the cycle it may not exist yet.

open as a page

A circular import between `routing.planner` and `routing.solver` fails only when the solver is imported first — why is a cycle order-dependent, and how do you find its entry point?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Whichever module is imported first is the one left half-built, so the cycle only breaks when the name the other module needs sits below the line that starts it. Reproduce each entry order in a fresh process.

open as a page

How does `typing.TYPE_CHECKING` break an import cycle caused only by type annotations?

level: seniorimportance: should knowfreq 42%

basics

~10 s

typing.TYPE_CHECKING is False at run time and True for type checkers, so an import inside if TYPE_CHECKING: never executes and cannot create a cycle, while the checker still sees the name for annotations.

open as a page