🛠️ import-scanner: Detect and remove unused imports in Python projects with patch output.

python dev.to

import-scanner

import-scanner is a lightweight command-line tool that analyzes Python projects to detect unused imports and generates a clean patch file showing exactly which lines can be removed. It helps maintain clean, readable, and efficient code by identifying imports that are declared but never used in the module.

Features

  • Scans all .py files in a given directory (recursively)
  • Uses Python's built-in ast module to parse code safely
  • Detects unused import and from ... import ... statements
  • Outputs a standard unified diff (patch) format for review or application
  • Safe: does not modify files directly; lets you review changes first
  • No external dependencies — built entirely on Python standard library

Usage

Run the tool from the command line by specifying the target directory:

python main.py /path/to/python/project
Enter fullscreen mode Exit fullscreen mode

The tool will print a patch to stdout. You can save it to a file or pipe it:

python main.py . > unused-imports.patch
Enter fullscreen mode Exit fullscreen mode

To apply the patch:

patch -p1 < unused-imports.patch
Enter fullscreen mode Exit fullscreen mode

Example

Given a file test.py with:

import os
import sys
from math import ceil, floor

print(sys.version)
Enter fullscreen mode Exit fullscreen mode

The tool will detect that os, ceil, and floor are imported but unused, and generate a patch removing those lines.

How It Works

The tool parses each Python file using the ast module, collecting all import statements and all names used in the code. It then determines which imported names never appear in expressions, calls, or assignments. Imports determined to be unused are flagged for removal.

Limitations

  • Does not handle star imports (from module import *) safely
  • May miss imports used dynamically via getattr, globals(), etc.
  • Does not analyze跨-file dependencies (e.g., imports used only in tests or other modules)

License

MIT

import ast
import os
import sys
import argparse
from pathlib import Path

def find_unused_imports(file_path):
    with open(file_path, 'r', encoding='utf-8') as f:
        try:
            tree = ast.parse(f.read())
        except SyntaxError:
            return []

    imports = {}
    used_names = set()

    # Collect imports
    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                imports[alias.asname or alias.name.split('.')[0]] = node.lineno
        elif isinstance(node, ast.ImportFrom):
            for alias in node.names:
                name = alias.asname or alias.name
                imports[name] = node.lineno

    # Collect used names (simple resolution)
    for node in ast.walk(tree):
        if isinstance(node, ast.Name):
            used_names.add(node.id)
        elif isinstance(node, ast.Attribute):
            # Handle attributes like `sys.version` -> include `sys`
            if isinstance(node.value, ast.Name):
                used_names.add(node.value.id)

    # Find unused imports
    unused_lines = set()
    for name, lineno in imports.items():
        if name not in used_names:
            unused_lines.add(lineno)

    return sorted(unused_lines)

def generate_patch(directory):
    dir_path = Path(directory)
    for pyfile in dir_path.rglob('*.py'):
        unused_lines = find_unused_imports(pyfile)
        if not unused_lines:
            continue

        rel_path = pyfile.relative_to(dir_path)
        with open(pyfile, 'r') as f:
            lines = f.readlines()

        print(f"--- {rel_path}")
        print(f"+++ {rel_path}")
        for line_num in unused_lines:
            line = lines[line_num - 1].rstrip()
            print(f"-{{line}}")


def main():
    parser = argparse.ArgumentParser(description='Find unused imports and generate patch.')
    parser.add_argument('directory', type=str, help='Project directory to scan')
    args = parser.parse_args()

    generate_patch(args.directory)

if __name__ == '__main__':
    main()
Enter fullscreen mode Exit fullscreen mode

Source: dev.to

arrow_back Back to Tutorials