In a circular import, why does `import mod` survive where `from mod import name` fails?
answer
- One binds an object, one binds an attribute
- Module objects are filled in place
- The attribute read happens immediately
- Import time versus call time
- Function-local imports run after everything is built
basics
~20 simport 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.
solid answer
~50 sThe two forms differ in **what they bind and when**. `import mod` binds one name to the module object itself. That object is created and cached before its body runs, and it is mutated in place as the body executes — the identity never changes — so holding a reference to a half-built module is harmless as long as you do not read attributes off it until it is finished. `from mod import name` is an eager snapshot: it fetches the attribute at import time and binds the resulting object into your namespace, so if `name` has not been defined yet you get `ImportError`. The practical rule for a cycle you cannot immediately remove is: import the module, not the name, and touch `mod.name` inside functions rather than at module level. As a bonus, that form also picks up later rebinding of `mod.name`, which the `from` form never does.
code
python · 10 linesimport sys, tempfile, pathlib
d = pathlib.Path(tempfile.mkdtemp())
(d / "planner.py").write_text("import solver\ndef plan():\n return solver.solve()\n")
(d / "solver.py").write_text("import planner\ndef solve():\n return 'route'\n")
sys.path.insert(0, str(d))
import planner
print(planner.plan())
print(sys.modules["solver"].planner is planner)go deeper
Remember the practical rule rather than the theory: in a cycle, write import mod and use mod.thing inside a function. Avoid from mod import thing at the top of a file that is part of the cycle.
Explain the difference in binding — a module object versus a snapshot of one attribute — and why an object that is filled in place can safely be referenced early. Name the AttributeError trap of reading through the module at import time.
Judge when the deferred import is an acceptable local fix and when it is technical debt: the hidden dependency, the failure moving to first call, and the fact that neither form removes the underlying coupling.
Frame the choice for a codebase: a convention of module-level import module styling makes cycles survivable, but tolerating them as a standard practice is what lets the dependency graph rot.
### Two statements, two different bindings `import solver` compiles to: run the import machinery for `solver`, then bind the local name `solver` to `sys.modules['solver']`. `from solver import solve` compiles to: run the import machinery for `solver`, then perform an attribute read equivalent to `getattr(sys.modules['solver'], 'solve')`, and bind the *result* to the local name `solve`. Everything about circular imports follows from that difference. ### Why binding a half-built module is safe A module object is a mutable namespace with a `__dict__`. The loader creates it empty, caches it, then executes the body, and every `def`, `class` and assignment writes into that same dict. The object's identity is fixed from the start; only its contents grow. So when the second module in a cycle runs `import planner` and gets back a module whose body is only one line in, it has captured a reference that will be complete by the time anyone uses it. Nothing is copied, nothing needs patching up afterwards. ### Why binding a name is not safe `from planner import plan` does not defer anything. It reads the attribute right there, during the window in which `planner` is half-built, and if `plan` is defined below the line that triggered the cycle, the read misses and the import machinery raises `ImportError: cannot import name 'plan' from partially initialized module 'planner' (most likely due to a circular import)`. ### The rule this produces For a cycle you cannot remove immediately: * import the **module**, never the name; * read `planner.plan` **inside a function or method**, not at module level. Both halves matter. `import planner` followed by a module-level `TARGET = planner.DEFAULT` fails just as hard, only with `AttributeError: partially initialized module 'planner' has no attribute 'DEFAULT' (most likely due to a circular import)` instead. Import time is the dangerous window; call time is not. ### The deferred (function-local) import The stronger version of the same idea is to move the import statement itself into the function: ```python def solve(): from planner import plan # runs on first call, long after import time return plan() ``` At call time `planner` is fully built, so even the `from` form works. Cost: an extra `sys.modules` dictionary lookup per call, which is negligible, plus two real drawbacks — the dependency becomes invisible to readers and to import-graph tooling, and any failure moves from process start to first use, which is a worse place to discover it. ### A second, unrelated consequence worth knowing Because `from mod import name` snapshots the object, the importing module keeps the original even if `mod.name` is rebound later. `import mod` plus `mod.name` at call time always sees the current value. That is why replacing a function for a test frequently has to target the module attribute rather than the copy an importing module already took. ### Submodules are a special case `import package.module` binds only the top name `package`; the submodule is reached as an attribute, which the import system sets on the parent package once the submodule finishes. During a cycle that attribute may not be set yet, so `package.module.thing` at module level can still fail. Since Python 3.7 the sibling form `from package import module` falls back to looking the submodule up in the module cache when the parent attribute is not yet set, which quietly fixed a whole family of package-internal cycles that used to fail on 3.6. ### Where this stops being a trick All of the above buys a working import; none of it removes the cycle. Two modules that need each other's names at run time are still mutually coupled — you cannot test, read or move one without the other. The durable answer is to lift whatever both need into a third module that imports neither, and let both depend on that.
- What does a function-local import cost on every call?After the first call, essentially one dictionary lookup: the import machinery finds the module already in `sys.modules` and binds it, with no file access or execution. The real costs are not performance — the dependency is hidden from readers and from import-graph tooling, and an import error surfaces on first call rather than at startup, which in a long-running worker can mean hours later.
- Does `import package.module` help in the same way inside a package cycle?Partly. It binds only the top-level `package` name, and the `module` attribute is set on the parent only once the submodule has finished executing, so a module-level `package.module.thing` can still fail mid-cycle. Reading it inside a function is safe. Since 3.7, `from package import module` also falls back to the module cache when the parent attribute is missing.
- Why can a module-level `import mod` still fail with AttributeError?The import statement itself succeeded — you have a valid but half-built module object. The failure is the next line: any module-level `mod.thing` is an eager attribute read inside exactly the same window, so it raises `AttributeError: partially initialized module ... has no attribute ...`. Binding the module is safe; reading through it at import time is not.
saying these in an interview costs you the question
- Says `import mod` works because Python imports it lazily
- Thinks `from mod import name` and `import mod` are interchangeable
- Believes the half-built module is later swapped for a new object
- Claims a function-local import re-reads the file on every call
- Reads mod.attribute at module level and calls the cycle fixed
- Assumes `from mod import name` tracks later rebinding of mod.name