Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

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 2 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

Sponsored Reserved space — layout preview until AdSense is connected

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.