Python Automation #1: Ending Repetitive Work — First Scripts and File Organizing
Open your Downloads folder and you’ll find months of screenshots, installers, PDF invoices, and zip archives piled up. Cleaning it takes ten minutes, and a week later it’s back to square one. Repetitive chores like this are exactly where Python pays off fastest. The goal of this series is to put a working automation script in the hands of someone who only knows basic Python syntax. If you’ve finished Modern Python Basics, you’re ready, and each post is sized so a working professional can follow along in spare moments. Here’s the map of all seven parts:
- #1 Ending repetitive work: first scripts and file organizing ← this post
- #2 Excel automation: handling repetitive reports with openpyxl
- #3 Web scraping basics: collecting data from static pages
- #4 Advanced web scraping: handling dynamic pages
- #5 Email and notifications: getting results delivered automatically
- #6 Scheduling: making it run on its own every day
- #7 Packaging as a CLI tool: wrapping up the series
The shape of an automation script #
An automation script looks different from a web service or a library. One file, a flow that reads top to bottom, and an if __name__ == "__main__": entry point at the very bottom — this simple structure is the baseline. The entry point block has a single job: it marks the code below to run only when this file is executed directly. Without it, the moment another script imports your file just to reuse a function, the organizing logic fires off. Automation scripts get reused often, so it’s worth building the habit of writing them in this shape from day one. You’ll see the concrete form in the complete example below.
How to run it #
The code in this post uses only the standard library, so there’s nothing to install. If Python is installed, run it from the terminal with python organize.py. If you’re using uv from Modern Python Basics #1, uv run organize.py gives the same result. The advantage of uv run shows up later, when you need external packages. Declare the dependencies at the top of the script and uv builds a temporary environment for you, so you can carry a single script file around without managing virtual environments.
Complete example: a Downloads folder organizer #
The standard tool for working with files is pathlib. It treats paths as objects rather than strings, so the same code works across operating systems. We need three operations: iterate over a folder’s contents with iterdir(), read the extension with suffix, and move with rename().
from pathlib import Path
RULES = {
"images": {".jpg", ".jpeg", ".png", ".gif", ".webp"},
"docs": {".pdf", ".docx", ".xlsx", ".pptx", ".txt"},
"archives": {".zip", ".tar", ".gz", ".7z"},
"installers": {".dmg", ".pkg", ".exe", ".msi"},
}
def category_for(item: Path) -> str | None:
for name, exts in RULES.items():
if item.suffix.lower() in exts:
return name
return None # leave extensions not in the rules alone
def organize(target: Path) -> None:
for item in target.iterdir():
if not item.is_file():
continue
category = category_for(item)
if category is None:
continue
dest_dir = target / category
dest_dir.mkdir(exist_ok=True)
item.rename(dest_dir / item.name)
print(f"{item.name} -> {category}/")
if __name__ == "__main__":
organize(Path.home() / "Downloads")The flow is simple: look at each file in the folder, and if its extension matches a rule, create the matching folder and move it there. mkdir(exist_ok=True) keeps the script from erroring when the folder already exists, and suffix.lower() makes sure uppercase extensions like .PDF match the same rule. Extensions not covered by the rules are left untouched, so there’s no risk of an unknown file getting shuffled off somewhere unexpected.
Preventing mistakes: dry-run and overwrites #
A script that moves files is hard to undo after one bad run, so two safety nets come first. The first is a dry-run mode: instead of actually moving anything, it only prints what it plans to move and where, letting you preview the result the first time you point it at a new folder. The second is overwrite protection. If a file with the same name already exists at the destination, rename can clobber it, so we’ll dodge collisions by appending a number, like report (1).pdf. Only the organize function needs to change:
def unique_path(dest: Path) -> Path:
if not dest.exists():
return dest
for n in range(1, 1000):
candidate = dest.with_name(f"{dest.stem} ({n}){dest.suffix}")
if not candidate.exists():
return candidate
raise RuntimeError("no available file name")
def organize(target: Path, dry_run: bool = False) -> None:
for item in target.iterdir():
if not item.is_file():
continue
category = category_for(item)
if category is None:
continue
dest_dir = target / category
dest = unique_path(dest_dir / item.name)
if dry_run:
print(f"[plan] {item.name} -> {category}/{dest.name}")
continue
dest_dir.mkdir(exist_ok=True)
item.rename(dest)
print(f"{item.name} -> {category}/{dest.name}")Call it with dry_run=True and it prints the plan, then stops. For any automation that touches files, this is the default order of operations: see the plan first, verify it, then execute.
Taking the target folder with argparse #
Right now the Downloads folder is hardcoded. Let’s wire up argparse so the folder to organize is passed as an argument at run time. It’s in the standard library — nothing to install — and it even generates the help text for you.
import argparse
def main() -> None:
parser = argparse.ArgumentParser(description="Sort files into folders by extension")
parser.add_argument("target", type=Path, help="path of the folder to organize")
parser.add_argument("--dry-run", action="store_true", help="print the plan without moving anything")
args = parser.parse_args()
organize(args.target, dry_run=args.dry_run)
if __name__ == "__main__":
main()Because we specified type=Path, the argument arrives as a Path object instead of a string. Now any folder can be the target, and the --dry-run flag gives you a preview.
python organize.py ~/Downloads --dry-run # [plan] invoice.pdf -> docs/invoice.pdf
python organize.py ~/Downloads # invoice.pdf -> docs/invoice.pdfOne step further: monthly folders and archiving old files #
Once sorting by extension feels comfortable, the same pattern yields variations. Swap the classification criterion from extension to the file’s modification time and you have a by-month organizer. Just replace category_for(item) inside organize with this function:
from datetime import datetime
def month_folder(item: Path) -> str:
mtime = datetime.fromtimestamp(item.stat().st_mtime)
return mtime.strftime("%Y-%m") # folder names like 2026-07Archiving old files comes from the same ingredients. Filter for files whose modification time is older than datetime.now() - timedelta(days=90), move them into an archive folder, and you have an archiving script. It’s one pattern — iterate, filter on a condition, move — with only the classification rule swapped out. Once this pattern is in your hands, most file-handling automation fits the same mold.
Wrap-up #
What this post built:
- The basic shape of an automation script: one file, top to bottom, an
if __name__ == "__main__":entry point - A Downloads folder organizer built on
pathlib’siterdir,suffix, andrename, with two safety nets — dry-run andunique_path - A structure that takes the target folder and flags as arguments via
argparse - A pattern that extends to monthly organizing and old-file archiving just by swapping the classification rule
In the next post (#2 Excel automation), we tackle the poster child of office busywork: Excel. We’ll use openpyxl to read multiple files, merge them into one, and apply formatting to produce a finished report.