Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Detect Circular Imports Across Python Projects Automatically

This script walks through all .py files in a directory, builds an import graph, and uses depth-first search to find cycles—printing each circular dependency chain.

Medium Python 3.9+ Jun 28, 2026 Automation & scripting 17 views 0 copies

Python code

63 lines
Python 3.9+
import ast
import sys
from pathlib import Path
from collections import defaultdict, deque

def find_imports(filepath):
    """Return set of module names imported by a Python file."""
    imports = set()
    try:
        with open(filepath) as f:
            tree = ast.parse(f.read())
    except (SyntaxError, UnicodeDecodeError):
        return imports
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                imports.add(alias.name.split('.')[0])
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                imports.add(node.module.split('.')[0])
    return imports

def detect_circular_imports(root_dir):
    """Detect circular imports in all .py files under root_dir."""
    root = Path(root_dir)
    files = list(root.rglob("*.py"))
    module_graph = defaultdict(set)
    for f in files:
        module_name = f.stem
        for imp in find_imports(f):
            module_graph[module_name].add(imp)
    visited = set()
    stack = set()
    cycles = []
    def dfs(node, path):
        visited.add(node)
        stack.add(node)
        for neighbor in module_graph.get(node, set()):
            if neighbor not in visited:
                if dfs(neighbor, path + [neighbor]):
                    return True
            elif neighbor in stack:
                cycle = path[path.index(neighbor):]
                cycles.append(cycle + [neighbor])
                return True
        stack.discard(node)
        return False
    for module in list(module_graph.keys()):
        if module not in visited:
            dfs(module, [module])
    return cycles

if __name__ == "__main__":
    if len(sys.argv) > 1:
        root_path = sys.argv[1]
    else:
        root_path = "."  # default current directory
    cycles = detect_circular_imports(root_path)
    if cycles:
        for cycle in cycles:
            print(" -> ".join(cycle))
    else:
        print("No circular imports detected.")

Output

stdout
module_a -> module_b -> module_a
module_x -> module_y -> module_z -> module_x

How it works

The script parses each Python file with ast to statically extract imported top-level module names. It builds a directed graph where edges go from a module to the modules it imports. A DFS with a recursion stack tracks nodes currently being explored; if a neighbor is already on the stack, a cycle has been found. The path list records the traversal order so the exact cycle can be reconstructed. Only first-level module names are considered (e.g., package.module becomes package) because circular imports typically involve top-level packages or files. This approach is fast and requires no runtime imports.

Common mistakes

  • Using `sys.modules` or actual imports instead of static AST analysis, which can run code and miss files not yet imported.
  • Forgetting to handle syntax errors or non-UTF-8 files, causing the script to crash early.
  • Assuming relative imports or multi-dot names correctly resolve without stripping submodules.
  • Not handling that modules with the same name in different subdirectories may be conflated if only `stem` is used as the key.

Variations

  1. Add `--exclude` argument to skip directories like `venv/` or `__pycache__` for faster scanning.
  2. Use `networkx` to run `simple_cycles()` for larger or deeper graphs in one call.

Real-world use cases

  • Running in CI/CD to block merges that introduce circular imports into a shared Python monorepo.
  • Analyzing a legacy project before refactoring to eliminate import cycles that cause brittle startup order.
  • Validating that new microservice libraries don't create circular dependencies across package boundaries.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Automation & scripting

Related tutorials and quizzes for this topic.