DP004: CircularImportCheck
Overview
| Property | Value |
|---|---|
| ID | DP004 |
| Name | CircularImportCheck |
| Group | dependencies |
| Severity | ERROR (top-level) / WARNING (conditional) |
Description
Detects circular imports in your Python package. Circular imports occur when module A imports module B, and module B (directly or indirectly) imports module A.
Why It Matters
Circular imports can cause:
- Runtime
ImportError: Python may fail with “cannot import name ‘X’” when the circular dependency is encountered - Partial Module State: One module may see an incomplete version of the other (missing attributes)
- Unpredictable Behavior: The order of imports affects which module is partially initialized
What It Checks
The check builds an import graph by parsing all Python files using the AST module, then uses depth-first search (DFS) to detect cycles.
Import Categories
| Category | Description | Severity |
|---|---|---|
| Top-level | import x at module scope |
ERROR |
| Conditional | Inside if TYPE_CHECKING: |
WARNING |
| Lazy | Inside functions | WARNING |
Example
# models.py
from validators import validate_user # This causes ImportError!
class User:
def validate(self):
return validate_user(self)
# validators.py
from models import User # This runs first
def validate_user(user: User) -> bool:
return isinstance(user, User)How to Fix
Option 1: Restructure modules
Move shared code to a third module that both can import:
# types.py
class User:
pass
# validators.py
from types import User
def validate_user(user: User) -> bool:
return isinstance(user, User)
# models.py
from types import User
from validators import validate_userOption 2: Use TYPE_CHECKING for type hints
# validators.py
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models import User # Only imported during type checking
def validate_user(user: "User") -> bool: # Use string annotation
return hasattr(user, 'name')Option 3: Use lazy imports
# validators.py
def validate_user(user):
from models import User # Imported when function is called
return isinstance(user, User)Configuration
Ignore specific modules
[tool.pycmdcheck.checks.DP004]
ignore_modules = ["mypackage.compat", "mypackage._internal"]Ignore by pattern
[tool.pycmdcheck.checks.DP004]
ignore_patterns = ["*._*", "*.compat"]Limit reported cycles
[tool.pycmdcheck.checks.DP004]
max_cycles = 5 # Only report first 5 cyclesSkip this check entirely
[tool.pycmdcheck]
skip = ["DP004"]