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:

  1. Runtime ImportError: Python may fail with “cannot import name ‘X’” when the circular dependency is encountered
  2. Partial Module State: One module may see an incomplete version of the other (missing attributes)
  3. 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_user

Option 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 cycles

Skip this check entirely

[tool.pycmdcheck]
skip = ["DP004"]