Circular imports occur when modules import each other; break the cycle with restructuring or deferred imports
A circular import happens when module A imports B and B imports A, directly or through a chain. Python starts executing A, reaches the import of B, starts executing B, which reaches the import of A. Since A is already in sys.modules but only partially initialized, the import may succeed but the name A is not yet defined, leading to an ImportError or an AttributeError later. The cleanest fix is to restructure so the cycle does not exist: move shared code into a third module that both can import, or use dependency injection so one module receives the other as a parameter instead of importing it. If restructuring is not practical, defer the import into the function or method where it is needed, so it runs after both modules have finished loading. Using from future import annotations or TYPE_CHECKING for type-only imports also avoids cycles at runtime.
Restructure: extract shared code into a lower-level module that neither A nor B depends on circularly.
Defer: move the import inside the function that needs it, so it happens at call time, not import time.
Type-only: use from typing import TYPE_CHECKING and guard imports that are only needed for annotations.
Dependency injection: pass the collaborator as an argument rather than importing the module.
Use importlib.import_module for dynamic, late-bound imports.
Common mistake: hiding the cycle with local imports everywhere, which makes the code harder to follow and can hide design problems.
Common mistake: assuming a partial import is safe because it did not raise. The name may be defined later, causing failures far from the import site.
Version note: circular imports behave the same across Python 3 versions. PEP 563 (postponed annotations) was optional and is not the default; 3.11+ still evaluates annotations unless from future import annotations is used.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience