NelsonLabs
Python Basics/Project: File Organiser Script

Project: File Organiser Script

Build a file organiser script that scans a directory, categorises files by type, and moves them into organised subfolders โ€” a genuinely useful automation tool.

organise.py โ€” complete file organiser
python
#!/usr/bin/env python3
"""
Organise files in a directory into categorised subfolders.
Usage: python organise.py /path/to/directory
"""
import os
import shutil
import sys
import json
from pathlib import Path
from datetime import datetime

# Category definitions
CATEGORIES = {
    "Images":     {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
    "Documents":  {".pdf", ".doc", ".docx", ".txt", ".md", ".csv", ".xlsx"},
    "Videos":     {".mp4", ".mov", ".avi", ".mkv", ".webm"},
    "Audio":      {".mp3", ".wav", ".flac", ".aac", ".ogg"},
    "Code":       {".py", ".js", ".ts", ".html", ".css", ".json", ".yaml"},
    "Archives":   {".zip", ".tar", ".gz", ".rar", ".7z"},
}

def get_category(extension: str) -> str:
    """Return the category for a file extension."""
    ext_lower = extension.lower()
    for category, extensions in CATEGORIES.items():
        if ext_lower in extensions:
            return category
    return "Other"

def organise_directory(target: Path, dry_run: bool = False) -> dict:
    """Organise files in target directory. Returns a summary report."""
    if not target.is_dir():
        raise ValueError(f"Not a directory: {target}")

    moved = {}
    skipped = []

    for file in target.iterdir():
        if not file.is_file() or file.name.startswith("."):
            continue

        category    = get_category(file.suffix)
        destination = target / category

        if not dry_run:
            destination.mkdir(exist_ok=True)

            # Handle duplicates
            dest_file = destination / file.name
            if dest_file.exists():
                stem = file.stem
                new_name = f"{stem}_{datetime.now().strftime('%H%M%S')}{file.suffix}"
                dest_file = destination / new_name

            shutil.move(str(file), str(dest_file))

        moved.setdefault(category, []).append(file.name)

    return {"moved": moved, "skipped": skipped, "dry_run": dry_run}

def print_report(report: dict, target: Path) -> None:
    """Print a formatted summary report."""
    print(f"
โ”€โ”€ File Organiser Report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€")
    print(f"Directory: {target}")
    print(f"Mode: {'Dry run (no changes made)' if report['dry_run'] else 'Live run'}
")

    if not report["moved"]:
        print("No files to organise.")
        return

    total = 0
    for category, files in sorted(report["moved"].items()):
        print(f"  {category}/  ({len(files)} files)")
        for f in files[:5]:   # show first 5
            print(f"    โ€ข {f}")
        if len(files) > 5:
            print(f"    ... and {len(files) - 5} more")
        total += len(files)

    print(f"
Total: {total} files organised.")

def main():
    if len(sys.argv) < 2:
        print("Usage: python organise.py <directory> [--dry-run]")
        sys.exit(1)

    target  = Path(sys.argv[1]).resolve()
    dry_run = "--dry-run" in sys.argv

    try:
        report = organise_directory(target, dry_run=dry_run)
        print_report(report, target)
    except ValueError as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

What you learned in this course

  • โ€”Python syntax โ€” indentation, variables, types
  • โ€”Operators โ€” arithmetic, comparison, logical, and identity
  • โ€”Conditionals with if/elif/else
  • โ€”Loops โ€” for, while, break, continue, and the else clause
  • โ€”Functions โ€” parameters, defaults, *args, **kwargs, lambda
  • โ€”Lists โ€” methods, slicing, and list comprehensions
  • โ€”Dictionaries and sets โ€” Python's key data structures
  • โ€”String methods and f-strings
  • โ€”File I/O with context managers, and JSON
  • โ€”Modules, pip, and virtual environments