Build a File Organizer CLI with Python
Build a Python CLI tool that auto-sorts files into folders using a TOML config.
Introduction
30 Second Summary
Your Downloads folder is probably a graveyard of random files. Reports, screenshots, songs, and zip files all piled together with no organization.
In this project, you will build a Python command-line tool that automatically sorts files into categorized subfolders based on rules you define in a configuration file.
What You'll Build
A CLI file organizer that scans any folder and moves files into subfolders based on their file extension, with sorting rules you control through a simple TOML config file.
By the end of this project, you'll have:
- A TOML configuration file that maps file extensions to destination folders.
- A multi-file Python project with separate modules for config loading, file operations, and CLI parsing.
- A dry-run mode that previews what would happen without actually moving anything.
- Secret Mission: Add a date-based sorting mode that organizes files by when they were last modified.
Are there any prerequisites?
You should be comfortable with basic Python concepts like loops, functions, and variables. No experience with file system operations or CLI tools is needed.
Before We Start
Before you can build the file organizer, you need Python 3.11 or higher installed on your machine.
This version introduced the tomllib module, which lets you read configuration files without installing any third-party packages. In this step, you'll verify your Python installation, create the project folder structure, and set up some test files to experiment with safely.
In this step, get ready to:
- Verify Python 3.11+ is installed.
- Create the project folder with three empty files.
- Set up a test folder with dummy files.
Verify your Python version
This project uses tomllib, a module that was added to Python in version 3.11. You need to confirm your installed version meets this minimum.
- Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Python version by running the command for your platform:
macOS
python3 --version
Windows
python --version
✔️ I see version 3.11 or higher
You should see output like Python 3.11.x or higher (e.g. Python 3.12.4). You're all set to continue.
ⓧ I see an older version
If you see a version lower than 3.11 (e.g. Python 3.9.7), you need to install a newer version.
On macOS, the system python3 often points to an older Apple-provided version even after you install a newer one from python.org. After installing, try running python3.11 --version or python3.14 --version to use the specific newer version directly.
- Download the latest Python from python.org/downloads.
- Run the installer and follow the prompts.
- Close and reopen your terminal, then run the version check again.
ⓧ Command not found
Python is not installed on your machine yet. You need to install it.
- Go to python.org/downloads and download the latest version.
- Run the installer. On Windows, make sure to check the Add Python to PATH checkbox during installation.
- Close and reopen your terminal, then run the version check again.
Create your project folder structure
Your file organizer will be a multi-file Python project. You'll create a folder on your Desktop with three separate files, each responsible for one concern: the entry point, the logic, and the configuration.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir file-organizer
cd file-organizer
- Create the three project files by running this command:
macOS
touch main.py organizer.py config.toml
Windows
New-Item main.py, organizer.py, config.toml
- Confirm the files exist by running:
ls
You should see main.py, organizer.py, and config.toml in the output.
Why three files?
Splitting a project into multiple files keeps each one focused on a single job. main.py handles the command-line interface, organizer.py contains the sorting logic, and config.toml stores the rules you can edit without touching code.
This separation of concerns is the standard pattern for real-world Python tools.
Not seeing all three files?
Make sure you ran the touch (macOS) or New-Item (Windows) command from inside the file-organizer folder. Run pwd (macOS) or cd (Windows) to confirm your current directory ends with file-organizer.
Help me figure out why my project files are missing.
Open the project in Zed and create test files
You'll use Zed as your code editor for this project. You also need a folder of dummy files to safely test your organizer without risking real documents.
- Open the file-organizer folder in Zed by running this command from inside the folder:
zed .
What if the zed command is not found?
If your terminal says zed is not recognized, you need to install the Zed CLI. Open Zed manually (from your Applications folder or Start menu), then press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) to open the command palette.
Type cli: install cli binary and press Enter. Close and reopen your terminal, then try zed . again.
- Create a test folder with dummy files by running these commands in your terminal:
macOS
mkdir test_mess
touch test_mess/report.pdf test_mess/photo.png test_mess/notes.txt test_mess/song.mp3
Windows
mkdir test_mess
New-Item test_mess/report.pdf, test_mess/photo.png, test_mess/notes.txt, test_mess/song.mp3
- Verify your test files exist by running:
ls test_mess
You should see report.pdf, photo.png, notes.txt, and song.mp3 listed.
Why use test files?
These are empty placeholder files that mimic what you'd find in a messy Downloads folder. Testing with dummy files means you can run your organizer freely without accidentally moving important documents.
Test files not appearing in Zed?
Zed may take a moment to detect new files created from the terminal. Check the file tree explorer on the left side of Zed (toggle it with Cmd+Shift+E on macOS or Ctrl+Shift+E on Windows). You should see test_mess/ as a subfolder.
Help me find my test files in Zed.
Your environment is ready. Next up, you'll write the configuration file that defines how your organizer sorts files into categories.
Write the Configuration File
Your project folder is set up with empty files and a test folder ready to go. Now it's time to define the rules that tell your organizer which file types belong where.
You'll use TOML, a simple configuration format built into Python 3.11+. This keeps your sorting rules separate from your code, so you can add new categories without touching any Python.
In this step, get ready to:
- Define file-sorting rules in a TOML configuration file.
- Write Python functions to load and parse those rules.
Define your sorting rules in config.toml
A TOML file uses a table-based format to group related settings together. Each category in your config will have a list of file extensions and a destination folder name.
- In Zed, open config.toml and paste the following sorting rules:
[categories]
[categories.Images]
extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp"]
folder = "Images"
[categories.Documents]
extensions = [".pdf", ".doc", ".docx", ".txt", ".xlsx", ".pptx", ".csv"]
folder = "Documents"
[categories.Audio]
extensions = [".mp3", ".wav", ".flac", ".aac", ".ogg"]
folder = "Audio"
[categories.Video]
extensions = [".mp4", ".avi", ".mkv", ".mov", ".wmv"]
folder = "Video"
[categories.Archives]
extensions = [".zip", ".rar", ".7z", ".tar", ".gz"]
folder = "Archives"
[categories.Code]
extensions = [".py", ".js", ".html", ".css", ".java", ".cpp", ".c"]
folder = "Code"
What does this config do?
The [categories] header is a top-level TOML table. Each sub-table like [categories.Images] defines one sorting category.
Inside each category, extensions lists which file types belong to it, and folder names the subfolder those files get moved into. Your Python code will read this structure and use it as a lookup table.
- Save config.toml.
- You should see six category sections in the file: Images, Documents, Audio, Video, Archives, and Code.
Seeing red squiggles in Zed?
Make sure each opening bracket has a matching closing bracket, and that extension strings use double quotes (not single quotes). TOML requires double-quoted strings.
Help me fix my TOML syntax errors
Write the config-loading function
Your config file is just a text file until Python reads it. You need a function that opens the TOML file, parses it into a Python dictionary, and handles the case where the file doesn't exist.
- Open organizer.py in Zed and paste the following imports and function:
import tomllib
import shutil
from pathlib import Path
def load_config(config_path):
"""Load sorting rules from a TOML config file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_file, "rb") as f:
config = tomllib.load(f)
return config["categories"]
What does this code do?
tomllib is Python's built-in TOML parser (available since 3.11). It requires files to be opened in binary mode ("rb"), which is why you see open(config_file, "rb") instead of plain open().
The function first checks whether the config file actually exists using Path.exists(). If the file is missing, it raises a clear FileNotFoundError instead of crashing with a confusing traceback.
pathlib and shutil are imported now for the file-moving logic you'll add in the next step.
You won't see a change yet. The next function will use load_config and make the full module testable.
- Add the build_extension_map function below load_config in organizer.py:
def build_extension_map(categories):
"""Convert categories config into a flat extension -> folder mapping."""
extension_map = {}
for category_name, category_data in categories.items():
folder = category_data["folder"]
for ext in category_data["extensions"]:
extension_map[ext.lower()] = folder
return extension_map
What does this function do?
The TOML config organizes extensions by category, which is great for humans to read. But your code needs the reverse lookup: given a file extension, which folder does it go in?
build_extension_map flattens the nested structure into a simple dictionary like {".png": "Images", ".pdf": "Documents", ...}. The .lower() call ensures matching works regardless of whether the file has .PNG or .png.
- Save organizer.py.
- Test that your module loads correctly by running this in your terminal:
python3 -c "from organizer import load_config, build_extension_map; cats = load_config('config.toml'); print(build_extension_map(cats))"
- You should see a dictionary printed in your terminal mapping extensions like .png to folder names like Images.
Seeing ModuleNotFoundError?
Make sure your terminal is inside the file-organizer folder. Python looks for organizer.py in the current directory. Run cd ~/Desktop/file-organizer and try again.
If you see ModuleNotFoundError: No module named 'tomllib', your Python version is below 3.11. Run python3 --version to confirm.
Help me debug this import error
✔️ Awesome, I've got everything!
Great. Double check you've saved both config.toml and organizer.py.
ⓧ I'd like to double check the full code
Here is the full config.toml:
[categories]
[categories.Images]
extensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp"]
folder = "Images"
[categories.Documents]
extensions = [".pdf", ".doc", ".docx", ".txt", ".xlsx", ".pptx", ".csv"]
folder = "Documents"
[categories.Audio]
extensions = [".mp3", ".wav", ".flac", ".aac", ".ogg"]
folder = "Audio"
[categories.Video]
extensions = [".mp4", ".avi", ".mkv", ".mov", ".wmv"]
folder = "Video"
[categories.Archives]
extensions = [".zip", ".rar", ".7z", ".tar", ".gz"]
folder = "Archives"
[categories.Code]
extensions = [".py", ".js", ".html", ".css", ".java", ".cpp", ".c"]
folder = "Code"
Here is the full organizer.py:
import tomllib
import shutil
from pathlib import Path
def load_config(config_path):
"""Load sorting rules from a TOML config file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_file, "rb") as f:
config = tomllib.load(f)
return config["categories"]
def build_extension_map(categories):
"""Convert categories config into a flat extension -> folder mapping."""
extension_map = {}
for category_name, category_data in categories.items():
folder = category_data["folder"]
for ext in category_data["extensions"]:
extension_map[ext.lower()] = folder
return extension_map
Your config and loading logic are in place. Next up, you'll write the core organizer functions that actually scan folders and move files.
Build the Core Organizer Logic
Your config file defines the rules, and your extension map flattens those rules into a quick-lookup dictionary. Now you need the functions that actually scan a folder and move files into the right place.
In this step, you will write the core engine of your file organizer. This includes scanning a directory for files, safely handling duplicate filenames, and moving files into categorized subfolders.
In this step, get ready to:
- Write a function that scans a folder and returns only files.
- Write a helper that prevents overwriting duplicate filenames.
- Write the main function that moves files into categorized subfolders with a dry-run safety mode.
Write a function to scan a folder for files
Before you can sort anything, you need a way to list all the files in a given folder. The function should ignore subdirectories and only return actual files.
- In Zed, switch back to organizer.py.
- Find the build_extension_map function, then add the following code below it:
def scan_folder(source_path):
"""Return a list of all files (not directories) in the source folder."""
source = Path(source_path)
if not source.exists():
raise FileNotFoundError(f"Source folder not found: {source_path}")
if not source.is_dir():
raise NotADirectoryError(f"Source path is not a folder: {source_path}")
files = [item for item in source.iterdir() if item.is_file()]
return files
What does this code do?
Path(source_path) converts the string path into a pathlib Path object, which gives you cross-platform file operations.
The two checks at the top raise clear errors if the folder does not exist or is not actually a directory. These guard against typos or wrong paths.
The list comprehension uses Path.iterdir() to loop through everything in the folder, then .is_file() filters out subdirectories so you only get actual files.
- Save organizer.py.
You will not see a visible change yet because you have not built the CLI that calls this function. The next chunk uses this to feed files into the sorting logic.
Seeing a red underline in Zed?
If Zed shows a syntax error, check that the function is at the top level (not indented inside another function). Also confirm there is a blank line between build_extension_map and this new function.
help me fix a syntax error in my scan_folder function
Handle duplicate filenames safely
What happens if you run the organizer twice and a file called photo.png already exists in the Images folder? Without protection, the existing file would be overwritten and lost forever.
- In organizer.py, add the following function below scan_folder:
def get_unique_destination(destination):
"""If a file already exists at destination, append a number to avoid overwriting."""
if not destination.exists():
return destination
stem = destination.stem
suffix = destination.suffix
parent = destination.parent
counter = 1
while destination.exists():
new_name = f"{stem}_{counter}{suffix}"
destination = parent / new_name
counter += 1
return destination
What does this code do?
This function checks if a file already exists at the target location. If the destination is free, it returns it unchanged.
If a conflict exists, it appends an incrementing number to the filename. For example, photo.png becomes photo_1.png, then photo_2.png, and so on until it finds a name that is not taken.
Path.stem gives you the filename without the extension. Path.suffix gives you the extension. Combining them with a counter builds the new safe name.
- Save organizer.py.
You will not see a change yet. The next function calls get_unique_destination every time it moves a file.
Not sure about the Path division operator?
The / operator on Path objects joins path segments. parent / new_name creates a new Path by combining the folder and the filename.
explain how pathlib uses the / operator to join paths
Write the move_files function with dry-run mode
This is the main engine of your organizer. It loops through every file, checks its extension against the rules, and moves it into the correct subfolder.
A dry-run mode lets you preview what the tool would do without actually moving anything. This is a safety feature found in most real-world CLI tools.
- In organizer.py, add the following function below get_unique_destination:
def move_files(source_path, extension_map, dry_run=False):
"""Move files from source into categorized subfolders based on extension map."""
source = Path(source_path)
files = scan_folder(source)
moved_count = 0
summary = {}
for file in files:
ext = file.suffix.lower()
if ext in extension_map:
folder_name = extension_map[ext]
dest_folder = source / folder_name
destination = dest_folder / file.name
destination = get_unique_destination(destination)
if dry_run:
print(f" [DRY RUN] {file.name} -> {folder_name}/")
else:
dest_folder.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(destination))
print(f" Moved: {file.name} -> {folder_name}/")
moved_count += 1
summary[folder_name] = summary.get(folder_name, 0) + 1
else:
print(f" Skipped: {file.name} (no matching rule)")
return moved_count, summary
What does this code do?
The function starts by calling scan_folder to get a list of files. Then it loops through each file and checks .suffix.lower() against the extension map.
When a match is found, the file's destination is calculated. get_unique_destination is called to prevent overwriting. If dry_run is True, it only prints what would happen. Otherwise it creates the subfolder with Path.mkdir(parents=True, exist_ok=True) and moves the file with shutil.move().
shutil.move() is used instead of Path.rename() because shutil works across different drives on Windows. Path.rename() would fail in that scenario.
Files with no matching extension are skipped with a printed message. The function returns a count and a summary dictionary so the CLI can display results.
- Save organizer.py.
You cannot run this function directly yet because you have not built main.py to call it. The next step wires everything together into a proper CLI.
Confused by the str() wrapping?
shutil.move(str(file), str(destination)) converts Path objects to strings. This ensures maximum compatibility across Python 3.11 through 3.14.
help me understand why shutil.move needs str() around Path objects
Why dry-run mode?
Most real-world CLI tools include a dry-run or preview mode. It builds trust by letting users see what would happen before committing to irreversible changes.
Moving files is a destructive operation. Dry-run mode gives you a chance to spot mistakes (like wrong config rules) before files actually move.
✔️ Awesome, I've got everything!
Great. Double check you have saved organizer.py before moving on to the next step.
ⓧ I'd like to double check the full code
Here is the complete organizer.py as it should look at the end of this step:
import tomllib
import shutil
from pathlib import Path
def load_config(config_path):
"""Load sorting rules from a TOML config file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Config file not found: {config_path}")
with open(config_file, "rb") as f:
config = tomllib.load(f)
return config["categories"]
def build_extension_map(categories):
"""Convert categories config into a flat extension -> folder mapping."""
extension_map = {}
for category_name, category_data in categories.items():
folder = category_data["folder"]
for ext in category_data["extensions"]:
extension_map[ext.lower()] = folder
return extension_map
def scan_folder(source_path):
"""Return a list of all files (not directories) in the source folder."""
source = Path(source_path)
if not source.exists():
raise FileNotFoundError(f"Source folder not found: {source_path}")
if not source.is_dir():
raise NotADirectoryError(f"Source path is not a folder: {source_path}")
files = [item for item in source.iterdir() if item.is_file()]
return files
def get_unique_destination(destination):
"""If a file already exists at destination, append a number to avoid overwriting."""
if not destination.exists():
return destination
stem = destination.stem
suffix = destination.suffix
parent = destination.parent
counter = 1
while destination.exists():
new_name = f"{stem}_{counter}{suffix}"
destination = parent / new_name
counter += 1
return destination
def move_files(source_path, extension_map, dry_run=False):
"""Move files from source into categorized subfolders based on extension map."""
source = Path(source_path)
files = scan_folder(source)
moved_count = 0
summary = {}
for file in files:
ext = file.suffix.lower()
if ext in extension_map:
folder_name = extension_map[ext]
dest_folder = source / folder_name
destination = dest_folder / file.name
destination = get_unique_destination(destination)
if dry_run:
print(f" [DRY RUN] {file.name} -> {folder_name}/")
else:
dest_folder.mkdir(parents=True, exist_ok=True)
shutil.move(str(file), str(destination))
print(f" Moved: {file.name} -> {folder_name}/")
moved_count += 1
summary[folder_name] = summary.get(folder_name, 0) + 1
else:
print(f" Skipped: {file.name} (no matching rule)")
return moved_count, summary
Your organizer engine is complete. Next up, you will build the CLI interface in main.py so you can actually run this tool from your terminal.
Build the CLI Interface
Your organizer logic is complete. You have functions that load config, map extensions, scan folders, and move files. Now you need a way for a user to actually run this tool from the terminal.
In this step, you will build a proper command-line interface using Python's built-in argparse module. This gives your tool named flags, helpful descriptions, and automatic error messages when someone uses it wrong.
In this step, get ready to:
- Set up an argparse CLI parser with source, config, and dry-run arguments.
- Wire the parsed arguments to your organizer functions and print a summary.
- Test your tool in both dry-run and live modes.
Set up the argparse CLI parser
The argparse module lets you define what arguments your script accepts. Users get helpful --help output automatically, and Python handles validation for you.
- In Zed, open main.py (currently empty) and paste the following code:
import argparse
from organizer import load_config, build_extension_map, move_files
def main():
# Create the argument parser with a description
parser = argparse.ArgumentParser(
description="Organize files in a folder by sorting them into categorized subfolders."
)
# Required positional argument: the folder to organize
parser.add_argument(
"source",
help="Path to the folder you want to organize"
)
# Optional flag: path to config file (defaults to config.toml)
parser.add_argument(
"--config",
default="config.toml",
help="Path to the TOML config file (default: config.toml)"
)
# Optional flag: preview mode without moving anything
parser.add_argument(
"--dry-run",
action="store_true",
help="Preview what would happen without actually moving files"
)
What does this code do?
The import at the top pulls in your organizer functions from organizer.py. Python finds this file automatically because it is in the same folder.
The parser definition sets up three arguments. source is positional (required, no dashes). --config is optional and defaults to config.toml. --dry-run uses action="store_true" which means it is False unless the user includes the flag.
- Save main.py.
- You won't see a change yet. The next chunk wires these arguments to your organizer functions.
Wire the arguments to your organizer functions
With the parser defined, you need to actually parse the user's input, call your organizer functions with the right values, and display a summary of what happened.
- In main.py, add the following code directly below the --dry-run argument (still inside the main() function):
args = parser.parse_args()
# Print a header so the user knows what's happening
print(f"\nFile Organizer")
print(f"{'=' * 40}")
print(f"Source: {args.source}")
print(f"Config: {args.config}")
print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE'}")
print(f"{'=' * 40}\n")
try:
# Load config and build the extension mapping
categories = load_config(args.config)
extension_map = build_extension_map(categories)
# Run the organizer with the user's chosen mode
moved_count, summary = move_files(args.source, extension_map, dry_run=args.dry_run)
# Print a summary of results
print(f"\n{'=' * 40}")
print(f"Summary: {moved_count} file(s) {'would be moved' if args.dry_run else 'moved'}")
for folder, count in summary.items():
print(f" {folder}: {count} file(s)")
except (FileNotFoundError, NotADirectoryError) as e:
print(f"Error: {e}")
except KeyError as e:
print(f"Config error: missing key {e}")
if __name__ == "__main__":
main()
What does this code do?
- parser.parse_args() reads whatever the user typed in the terminal and returns it as an object with attributes (args.source, args.config, args.dry_run).
- The try/except block catches errors from your organizer functions (missing files, bad config) and prints a friendly message instead of a scary traceback.
- The if __name__ == "__main__" guard ensures main() only runs when you execute the file directly, not when another script imports it.
- Save main.py.
- Verify argparse is working by running this in your terminal:
python main.py --help
You should see a usage message listing your three arguments (source, --config, --dry-run) with their descriptions.
Seeing a ModuleNotFoundError?
If you see an error about organizer not being found, make sure your terminal is inside the file-organizer folder. Run cd ~/Desktop/file-organizer to get there.
Also confirm that organizer.py is saved in the same folder as main.py. Python can only import modules from the same directory (or installed packages).
help me debug a ModuleNotFoundError when importing organizer in my Python CLI project
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
import argparse
from organizer import load_config, build_extension_map, move_files
def main():
parser = argparse.ArgumentParser(
description="Organize files in a folder by sorting them into categorized subfolders."
)
parser.add_argument(
"source",
help="Path to the folder you want to organize"
)
parser.add_argument(
"--config",
default="config.toml",
help="Path to the TOML config file (default: config.toml)"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Preview what would happen without actually moving files"
)
args = parser.parse_args()
print(f"\nFile Organizer")
print(f"{'=' * 40}")
print(f"Source: {args.source}")
print(f"Config: {args.config}")
print(f"Mode: {'DRY RUN' if args.dry_run else 'LIVE'}")
print(f"{'=' * 40}\n")
try:
categories = load_config(args.config)
extension_map = build_extension_map(categories)
moved_count, summary = move_files(args.source, extension_map, dry_run=args.dry_run)
print(f"\n{'=' * 40}")
print(f"Summary: {moved_count} file(s) {'would be moved' if args.dry_run else 'moved'}")
for folder, count in summary.items():
print(f" {folder}: {count} file(s)")
except (FileNotFoundError, NotADirectoryError) as e:
print(f"Error: {e}")
except KeyError as e:
print(f"Config error: missing key {e}")
if __name__ == "__main__":
main()
Test your tool in dry-run and live modes
The --dry-run flag lets you preview exactly what the tool will do before it touches any files. Always test with dry-run first.
- Run a dry-run test against your test_mess folder by running this command:
python main.py test_mess --dry-run
You should see output showing each file's intended destination without anything actually moving. The mode should say DRY RUN and the summary should say files "would be moved".
What should I see?
Your output should list lines like [DRY RUN] report.pdf -> Documents/ and [DRY RUN] photo.png -> Images/. The summary at the bottom shows the total count per folder.
- Now run the tool for real (without --dry-run) to actually move the files:
python main.py test_mess
- Open the test_mess folder in Finder (macOS) or File Explorer (Windows).
- Verify that new subfolders have been created: Documents/ (containing report.pdf and notes.txt), Images/ (containing photo.png), and Audio/ (containing song.mp3).
Files didn't move?
Make sure you are running the command from the file-organizer directory, not from inside test_mess. The source argument is a relative path from where you run the command.
If you see a config error, confirm config.toml is in the same folder you are running from. The default config path is relative to your current working directory.
help me troubleshoot why my file organizer tool isn't moving files into subfolders
Your file organizer is fully functional. You built a real CLI tool that reads config, scans folders, and moves files safely. Next up, the Secret Mission challenges you to add a date-based sorting mode.
Secret mission
Add Date-Based Sorting Mode
Your file organizer sorts by extension, but what if you want to sort by when files were created? Add a --by-date flag that organizes files into year-month subfolders based on their last-modified time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue experimenting, or delete them to free up disk space.
Resources you used:
- The file-organizer project folder on your Desktop (contains main.py, organizer.py, config.toml, and the test_mess folder).
Keep everything running
No action needed. Choose this if you want to keep experimenting with your file organizer, add new sorting rules, or point it at other folders.
- Your file-organizer folder stays on your Desktop, ready to use anytime.
- Run python main.py <any-folder> --dry-run whenever you want to preview an organization pass on a real folder.
- Python and Zed remain installed on your machine as general-purpose tools.
Pause - I'll come back to this later
Since this is a local project with no running processes, there is nothing to pause. Your files sit on disk without consuming resources until you run them again.
- Close the file-organizer folder in Zed.
- Your project folder stays on your Desktop whenever you want to return.
Delete - I don't want to use this again
Remove all project files from your machine. This cannot be undone.
- Close the file-organizer folder in Zed.
- Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/file-organizer
On Windows, run this instead:
rmdir /s /q %USERPROFILE%\Desktop\file-organizer
- Confirm the folder is gone by running:
ls ~/Desktop | grep file-organizer
You should see no output, confirming the folder has been removed.
Nice Work!
Nice Work!
Nicely done. You just built a real automation tool from scratch that keeps your folders tidy with a single terminal command.
You've learned how to:
- Structure a multi-file Python project with proper separation of concerns across main.py, organizer.py, and config.toml.
- Use pathlib for cross-platform file path handling and shutil.move() for safe file moving that works across drives on Windows.
- Build a proper CLI with argparse including flags, defaults, help text, and real-world edge case handling like duplicate filenames and --dry-run safety.
- Secret Mission: Added a date-based sorting mode using file metadata and datetime.
Ready to quiz yourself?