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
.pyfiles in a given directory (recursively) - Uses Python's built-in
astmodule to parse code safely - Detects unused
importandfrom ... 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
The tool will print a patch to stdout. You can save it to a file or pipe it:
python main.py . > unused-imports.patch
To apply the patch:
patch -p1 < unused-imports.patch
Example
Given a file test.py with:
import os
import sys
from math import ceil, floor
print(sys.version)
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()