---
title: "Auto-organize your downloads folder with Python"
description: "A script that sorts files into folders by type, handles name clashes safely, and can keep watching for new arrivals. Dry-run first, so nothing is ever lost."
url: https://articles.sythra.ai/articles/python-auto-organize-files
slug: python-auto-organize-files
author: "vaibhavkothari"
author_url: https://articles.sythra.ai/writers/vaibhavkothari
date_published: 2026-08-26T08:38:08.169Z
date_modified: 2026-08-29T10:29:25.265Z
topics: ["Python", "Coding", "Learning"]
reading_time_minutes: 6
publisher: "Sythra"
publisher_url: https://sythra.ai
access: free
language: en
---

# Auto-organize your downloads folder with Python

> A script that sorts files into folders by type, handles name clashes safely, and can keep watching for new arrivals. Dry-run first, so nothing is ever lost.

Source: https://articles.sythra.ai/articles/python-auto-organize-files · Author: vaibhavkothari · Published: 2026-08-26 · Reading time: 6 min · Topics: Python, Coding, Learning

Your Downloads folder has 900 files. Screenshots, invoices, three copies of the same installer, a dataset you needed once.

Twenty lines of Python can sort it into folders in under a second — and with a dry run first, so you see exactly what will happen before anything moves.

## What you will build

1. A script that sorts files into `Images`, `Documents`, `Video` and so on
2. A dry-run mode that only prints its plan
3. Safe renaming when a file with that name already exists
4. Optional date-based subfolders
5. An optional watcher that files new downloads as they arrive

## Step 1 — Look before you touch

```python
from pathlib import Path
from collections import Counter

folder = Path.home() / "Downloads"

files = [f for f in folder.iterdir() if f.is_file()]
print(f"{len(files)} files")

extensions = Counter(f.suffix.lower() for f in files)
for ext, count in extensions.most_common(15):
    print(f"{ext or '(none)':10} {count}")
```

`Path.home()` finds your home directory on any operating system, so this script works unchanged on Windows, macOS and Linux. Run this first — the output tells you which categories you actually need.

## Step 2 — Map extensions to folders

```python
CATEGORIES = {
    "Images": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".heic", ".bmp"},
    "Documents": {".pdf", ".docx", ".doc", ".txt", ".md", ".odt", ".rtf"},
    "Spreadsheets": {".xlsx", ".xls", ".csv", ".ods"},
    "Slides": {".pptx", ".ppt", ".odp"},
    "Video": {".mp4", ".mov", ".avi", ".mkv", ".webm"},
    "Audio": {".mp3", ".wav", ".m4a", ".flac", ".aac"},
    "Archives": {".zip", ".rar", ".7z", ".tar", ".gz"},
    "Installers": {".exe", ".msi", ".dmg", ".deb", ".pkg"},
    "Code": {".py", ".js", ".ts", ".ipynb", ".json", ".html", ".css"},
}

def category_for(path):
    suffix = path.suffix.lower()
    for name, extensions in CATEGORIES.items():
        if suffix in extensions:
            return name
    return "Other"
```

Sets are used deliberately: `in` on a set is instant regardless of size, unlike scanning a list.

## Step 3 — Never overwrite a file

The dangerous part of any move script. If `report.pdf` already exists in the destination, `shutil.move` will happily replace it. Add a counter instead:

```python
def unique_path(target):
    if not target.exists():
        return target

    stem, suffix, parent = target.stem, target.suffix, target.parent
    counter = 2
    while True:
        candidate = parent / f"{stem} ({counter}){suffix}"
        if not candidate.exists():
            return candidate
        counter += 1
```

`report.pdf` becomes `report (2).pdf`. Nothing is ever lost — the same safety idea as the dry-run mode in [our file renaming guide](https://articles.sythra.ai/articles/python-rename-files-script).

## Step 4 — Move files, dry-run first

```python
import shutil
from pathlib import Path

def organize(folder, dry_run=True):
    folder = Path(folder)
    moved = 0

    for item in folder.iterdir():
        if not item.is_file() or item.name.startswith("."):
            continue

        destination_dir = folder / category_for(item)
        destination = unique_path(destination_dir / item.name)

        if dry_run:
            print(f"[dry-run] {item.name} -> {destination_dir.name}/{destination.name}")
        else:
            destination_dir.mkdir(exist_ok=True)
            shutil.move(str(item), str(destination))
            print(f"[moved] {item.name} -> {destination_dir.name}/{destination.name}")

        moved += 1

    print(f"\n{moved} files {'would be ' if dry_run else ''}organized.")


if __name__ == "__main__":
    organize(Path.home() / "Downloads", dry_run=True)
```

`dry_run=True` is the default on purpose. Read the output, confirm it looks right, then flip it to `False`.

Two guards matter more than they look. Skipping directories stops the script trying to move the category folders it just created. Skipping names starting with `.` protects hidden system files.

## Step 5 — Add date subfolders

Useful for screenshots and invoices, where you remember roughly *when* rather than *what*:

```python
from datetime import datetime

def dated_destination(folder, item, by_date=False):
    base = folder / category_for(item)
    if not by_date:
        return base

    modified = datetime.fromtimestamp(item.stat().st_mtime)
    return base / f"{modified:%Y}" / f"{modified:%m-%B}"
```

This produces `Images/2026/08-August/photo.jpg`. Create nested folders with `mkdir(parents=True, exist_ok=True)`.

## Step 6 — Handle files still downloading

Moving a half-downloaded file corrupts it. Skip browser temp files and anything modified in the last few seconds:

```python
import time

SKIP_SUFFIXES = {".crdownload", ".part", ".tmp", ".download"}

def is_ready(item, quiet_seconds=5):
    if item.suffix.lower() in SKIP_SUFFIXES:
        return False
    return (time.time() - item.stat().st_mtime) > quiet_seconds
```

Call `is_ready(item)` in the loop and `continue` when it returns False.

## Step 7 — Watch the folder continuously

```bash
python -m pip install watchdog
```

```python
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class Sorter(FileSystemEventHandler):
    def __init__(self, folder):
        self.folder = Path(folder)

    def on_created(self, event):
        if event.is_directory:
            return

        item = Path(event.src_path)
        time.sleep(3)          # let the download finish

        if not item.exists() or not is_ready(item):
            return

        destination_dir = self.folder / category_for(item)
        destination_dir.mkdir(exist_ok=True)
        shutil.move(str(item), str(unique_path(destination_dir / item.name)))
        print(f"Filed: {item.name} -> {destination_dir.name}/")


if __name__ == "__main__":
    downloads = Path.home() / "Downloads"

    observer = Observer()
    observer.schedule(Sorter(downloads), str(downloads), recursive=False)
    observer.start()
    print("Watching. Ctrl+C to stop.")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()
```

Now every new download files itself. `recursive=False` keeps it watching only the top level, so it ignores the folders it creates.

## Step 8 — Run it automatically

**Windows** — Task Scheduler → Create Basic Task → Daily → Start a program:

```text
Program:   C:\Path\To\.venv\Scripts\python.exe
Arguments: C:\Path\To\organize.py
```

**macOS / Linux** — `crontab -e`, then run daily at 6pm:

```text
0 18 * * * /path/to/.venv/bin/python /path/to/organize.py
```

Use the full path to the Python inside your environment. A bare `python` in a scheduled task often resolves to a different interpreter — the same class of problem behind ['pip' is not recognized](https://articles.sythra.ai/articles/pip-not-recognized-windows-fix).

## Common problems (and fixes)

| Problem | Fix |
|---|---|
| `PermissionError` | File is open in another app; close it and rerun |
| Corrupted downloads | Add the `is_ready` check from Step 6 |
| Script moves its own folders | Skip directories with `item.is_file()` |
| Hidden files disappear | Skip names starting with `.` |
| Watcher fires too early | Increase the `time.sleep` in `on_created` |
| Nothing happens | `dry_run` is still `True` — that is intentional |

## What you learned

- `pathlib` for paths that work on every operating system
- Sets for instant extension lookup
- Safe renaming instead of overwriting
- Dry-run as a default for anything destructive
- Watching a folder for changes with `watchdog`
- Scheduling a script to run on its own

```remember
# Remember this
`pathlib` -> paths that work on every operating system
A set of extensions -> instant lookup, no long if-chains
`p.rename(target)` -> silently overwrites, so check first
---
Dry run by default. Destructive scripts earn trust before they get it.
```


## FAQ

### How do I automatically organize files by type in Python?

Loop over the folder with `pathlib`, map each file's extension to a category, create the category folder with `mkdir(exist_ok=True)`, and move the file with `shutil.move`. Run it in dry-run mode first.

### How do I avoid overwriting files when moving them?

Check whether the destination exists and, if it does, append a counter to the filename — `report (2).pdf` — until you find a name that is free.

### How do I stop the script moving half-downloaded files?

Skip `.crdownload`, `.part` and `.tmp` suffixes, and ignore any file modified within the last few seconds.

### Can Python watch a folder and sort files automatically?

Yes. Install `watchdog`, subclass `FileSystemEventHandler`, and handle `on_created` with a short delay so the download finishes before the file is moved.

### How do I run a Python script on a schedule?

Use Task Scheduler on Windows or cron on macOS and Linux, and give the full path to the Python interpreter inside your virtual environment rather than a bare `python`.

## Next reading on Sythra Articles

- [Stop renaming files by hand](https://articles.sythra.ai/articles/python-rename-files-script)
- ['pip' is not recognized — fix it on Windows](https://articles.sythra.ai/articles/pip-not-recognized-windows-fix)
- [Your first Python program: say hello in 5 minutes](https://articles.sythra.ai/articles/first-python-hello)
