Unit 5 Project: A Safe File Organizer

Put it together — argparse, pathlib, dry-run, and tests

Intermediate 13 min

In this lesson

In this project you’ll build a file organizer in Python: a command-line tool that sorts files into folders by type. The design splits cleanly into a pure planning core you can test and a thin CLI that runs it — with a --dry-run flag so nothing moves until you’ve seen the plan.

Explain it like I’m 5

This tool is a careful file helper: first it tells you exactly what it would do, and it only does it for real once you’re happy with the plan.

Plan the tool: separate deciding from doing

The key design move is to keep the decision (where should this file go?) separate from the action (actually move it). The decision is a pure helper with no side effects, so it’s trivial to test. target_folder returns a type folder, falling back to "other" for files with no extension.

Example
from pathlib import Path

def target_folder(name):
    """Pure helper: where should this file go? Easy to test."""
    ext = Path(name).suffix.lower().lstrip(".")
    return ext or "other"

print(target_folder("Report.PDF"))
print(target_folder("README"))
Output
pdf
other
A pure helper with no side effects — the part worth testing.

Build the dry-run plan

With the decision isolated, the plan builder just maps every file to a destination line. It returns data — a list of strings — rather than printing, so the caller decides what to do with it (show it, log it, count it).

Example
from pathlib import Path

def target_folder(name):
    return Path(name).suffix.lower().lstrip(".") or "other"

def plan_moves(files):
    return [f"{name} -> {target_folder(name)}/{name}" for name in files]

for line in plan_moves(["a.txt", "pic.JPG", "notes"]):
    print(line)
Output
a.txt -> txt/a.txt
pic.JPG -> jpg/pic.JPG
notes -> other/notes
The plan is just data — easy to print, log, or test.

Wire up the CLI with --dry-run

The command-line layer is thin: parse a --dry-run flag, and either print the plan or carry out the moves. argparse turns --dry-run into args.dry_run (dashes become underscores).

Finish the tool: when --dry-run is passed, print each line of plan_moves(files). (We supply the args and file list for you.)

import argparse
from pathlib import Path

def target_folder(name):
    return Path(name).suffix.lower().lstrip(".") or "other"

def plan_moves(files):
    return [f"{name} -> {target_folder(name)}/{name}" for name in files]

files = ["a.txt", "b.jpg"]
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(["--dry-run"])

if args.dry_run:
    pass  # TODO: print each line of plan_moves(files)
else:
    print(f"Would move {len(files)} files")

Common mistake: Moving files for real before the dry run looks right

Why it happens:

The plan and the move are easy to wire up at once, so the safety preview gets skipped.

How to fix it:

Default to dry-run output and require an explicit flag (or confirmation) to perform real moves. See the plan first, every time.

Common mistake: Mixing planning with the actual moving

Why it happens:

Doing the move inside the loop that decides the destination feels shorter.

How to fix it:

Keep target_folder/plan_moves pure and put the real file operations in a separate function. Only the pure part is easy to test.

Common mistake: Testing on real, important folders

Why it happens:

The actual files are right there, so they get used as test data.

How to fix it:

Test the pure helpers, or run against a folder of throwaway samples. Never let a test move files you care about.

Why separate plan_moves() from the code that actually moves files?

What should --dry-run do?

argparse turns the flag --dry-run into which attribute?

Mini exercise (hard)

Build the planning core of a file organizer: target_folder(name) returns the lowercase extension (or "other" if none), and plan_moves(files) returns a list of "name -> folder/name" strings. Prove it on the given files.

Do it here. Finish the code below and press Run to try your answer right in the browser — no setup needed.

from pathlib import Path

def target_folder(name):
    # TODO: return the lowercase extension without the dot, or "other" if none
    return "other"

def plan_moves(files):
    # TODO: return a "name -> folder/name" string for each file
    return []

print(plan_moves(["song.MP3", "data.csv", "LICENSE"]))

What to learn next

You pulled Unit 5 together into one tool: a pure target_folder() helper, a testable plan_moves() builder, and a thin argparse CLI with a --dry-run flag so nothing moves until you’ve seen the plan. That deciding-versus-doing split is the habit that makes automation safe.

That wraps the practical-scripting unit. The Intermediate path now turns to making Python answer web requests. How Websites Use Python opens Unit 6: the browser/server model and the mental picture behind every Flask app. When you’re ready, continue to the next lesson.