Introduction
A circular import occurs when module A imports module B, and module B imports module A (directly or through a chain of imports). Python raises ImportError or AttributeError because one module's namespace is not fully populated when the other tries to access it.
This is one of the most common structural errors in growing Python codebases, especially when models, services, and utilities become tightly coupled.
Symptoms
- ImportError: cannot import name 'X' from partially initialized module 'Y'
- AttributeError: partially initialized module has no attribute 'SomeClass'
- Module works in isolation but fails when imported as part of the application
Common Causes
- Two modules import each other at the top level
- A chain of imports creates a cycle: A -> B -> C -> A
- Refactoring moved code without updating import dependencies
Step-by-Step Fix
- 1.Identify the circular dependency chain: Use Python's import tracing to find the cycle.
- 2.```python
- 3.python -c "import sys; sys.setrecursionlimit(50); import your_module"
- 4.# Or use the importlib metadata:
- 5.python -c "
- 6.import importlib
- 7.import sys
- 8.try:
- 9.import your_app.main
- 10.except ImportError as e:
- 11.print(f'Circular import detected: {e}')
- 12."
- 13.
` - 14.Move one import inside a function (lazy import): Break the cycle by deferring one import.
- 15.```python
- 16.# BEFORE (circular):
- 17.# module_a.py
- 18.from module_b import helper_b
- 19.def func_a():
- 20.return helper_b()
# module_b.py from module_a import func_a def helper_b(): return func_a()
# AFTER (lazy import breaks cycle): # module_b.py def helper_b(): from module_a import func_a # Imported at call time return func_a() ```
- 1.Extract shared types to a third module: Create a common module that both can import from.
- 2.```python
- 3.# types.py - shared definitions
- 4.class User:
- 5.pass
class Order: pass
# module_a.py from types import User def process_user(user: User): ...
# module_b.py from types import Order def process_order(order: Order): ... ```
- 1.Use TYPE_CHECKING for type hints only: Break cycles caused by type annotations.
- 2.```python
- 3.from __future__ import annotations
- 4.from typing import TYPE_CHECKING
if TYPE_CHECKING: from module_b import ClassB # Only imported by type checkers, not at runtime
class ClassA: def method(self, obj: ClassB) -> None: pass # ClassB not needed at runtime ```
Prevention
- Design module dependencies as a directed acyclic graph (DAG)
- Use TYPE_CHECKING blocks for cross-module type annotations
- Run pylint or pyright to detect circular imports before they cause runtime errors
- Extract shared interfaces and types into a dedicated module early in project growth