Free tools Windows power users keep installed
One-click scans. No signup required.
Seven chores are worth automating with nothing but the Python standard library: batch renaming, sorting a folder, dated backups, ZIP archiving, CSV cleanup, repeatable reports, and calling a trusted external tool. Each script below is short and bounded, and each one that changes files previews first. No pip installs are needed.
One caveat up front: no source reviewed for this article measured how much time scripts like these save, so none is promised. The code follows behavior documented in Python’s tutorial and library reference, but it is example code, not a tested product. Run every script on a copy of your data before trusting it.
Ground rules that apply to all seven
- Preview by default. Scripts that rename, move, or write files print what they would do and only act when you pass
--apply. - Explicit paths. Folders come from command-line arguments, not from “whatever directory I happen to be in”.
- Never overwrite silently. Each script checks for name collisions and skips instead of replacing.
- Keep originals until you have checked the output. Cleanup and deletion stay manual.
Requires Python 3.8 or newer for the code shown. Save each script as its own file and run it with python script_name.py --help to see its options.
1. Batch rename files
Use it when a folder holds files such as IMG_4471.jpg and you want trip-001.jpg, trip-002.jpg, and so on. pathlib handles the paths; the rename itself is Path.rename.
#1 Best Overall
import argparse
from pathlib import Path
parser = argparse.ArgumentParser(description="Rename .jpg files to PREFIX001.jpg, PREFIX002.jpg ...")
parser.add_argument("folder", type=Path)
parser.add_argument("--prefix", default="trip-")
parser.add_argument("--apply", action="store_true", help="actually rename (default is preview)")
args = parser.parse_args()
if not args.folder.is_dir():
raise SystemExit(f"Not a folder: {args.folder}")
files = sorted(args.folder.glob("*.jpg"))
for number, old in enumerate(files, start=1):
new = old.with_name(f"{args.prefix}{number:03d}{old.suffix}")
if new == old:
continue
if new.exists():
print(f"SKIP (target exists): {old.name} -> {new.name}")
continue
print(f"{old.name} -> {new.name}")
if args.apply:
old.rename(new)
Expected result: a preview list of old and new names. Re-run with --apply once it looks right. Note that glob("*.jpg") is case-sensitive on Linux, so .JPG files will not match there.
2. Sort a downloads or project folder
Use it when a folder has become a dumping ground. Match by extension, keep the category list small, and move with shutil.move.
import argparse
import shutil
from pathlib import Path
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".gif"},
"Documents": {".pdf", ".docx", ".txt", ".xlsx"},
"Archives": {".zip", ".tar", ".gz"},
}
parser = argparse.ArgumentParser(description="Move files into category subfolders.")
parser.add_argument("folder", type=Path)
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
if not args.folder.is_dir():
raise SystemExit(f"Not a folder: {args.folder}")
for item in sorted(args.folder.iterdir()):
if not item.is_file():
continue
for category, extensions in CATEGORIES.items():
if item.suffix.lower() in extensions:
target_dir = args.folder / category
target = target_dir / item.name
if target.exists():
print(f"SKIP (exists): {target}")
else:
print(f"{item.name} -> {category}/")
if args.apply:
target_dir.mkdir(exist_ok=True)
shutil.move(str(item), str(target))
break
Files that match no category stay where they are, which is the safe default. Because only top-level files are examined, existing subfolders are not touched.
Rank #2
3. Make a dated backup copy
Use it when you are about to edit or clean a folder and want a snapshot first. shutil.copytree copies a directory tree; by default it uses copy2, which tries to keep timestamps and permission bits.
import argparse
import shutil
from datetime import datetime
from pathlib import Path
parser = argparse.ArgumentParser(description="Copy a folder to a timestamped backup.")
parser.add_argument("source", type=Path)
parser.add_argument("backup_root", type=Path)
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
if not args.source.is_dir():
raise SystemExit(f"Source not found: {args.source}")
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
destination = args.backup_root / f"{args.source.name}-{stamp}"
print(f"{args.source} -> {destination}")
if args.apply:
args.backup_root.mkdir(parents=True, exist_ok=True)
shutil.copytree(args.source, destination)
print("Done.")
Limit to know about: Python’s documentation notes that its copy functions cannot preserve every kind of metadata on every platform (for example, some operating-system-specific attributes). Treat this as a file-level safety copy, not a full system clone. Keep the backup folder outside the source folder, or the copy will try to include itself on later runs.
4. Archive a finished project folder
Use it when a project is closed and you want one tidy ZIP. The zipfile module writes the archive; this script then verifies it before you decide to delete anything.
import argparse
import zipfile
from pathlib import Path
parser = argparse.ArgumentParser(description="Zip a folder and verify the archive.")
parser.add_argument("folder", type=Path)
parser.add_argument("output", type=Path, help="e.g. archive/project-2026.zip")
parser.add_argument("--apply", action="store_true")
args = parser.parse_args()
if not args.folder.is_dir():
raise SystemExit(f"Not a folder: {args.folder}")
if args.output.exists():
raise SystemExit(f"Refusing to overwrite: {args.output}")
files = [p for p in sorted(args.folder.rglob("*")) if p.is_file()]
print(f"{len(files)} files would be added to {args.output}")
if args.apply:
args.output.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(args.output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for path in files:
zf.write(path, arcname=path.relative_to(args.folder.parent))
with zipfile.ZipFile(args.output) as zf:
bad = zf.testzip()
if bad or len(zf.namelist()) != len(files):
raise SystemExit("Verification failed - keep the originals.")
print("Archive verified. The source folder was not deleted.")
Write the ZIP somewhere outside the folder being archived. The script deliberately never deletes the source; do that by hand after opening the archive and spot-checking a few files. Empty directories are not stored because only files are added.
5. Clean or combine CSV exports
Use it when an export has stray whitespace, inconsistent casing, or duplicate rows. The csv module is enough for row-level cleanup, so pandas is unnecessary here. This example assumes columns named name and email; change them to match your file.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →import argparse
import csv
from pathlib import Path
parser = argparse.ArgumentParser(description="Normalize and de-duplicate a CSV by email.")
parser.add_argument("input", type=Path, nargs="+", help="one or more CSV files")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.output.exists():
raise SystemExit(f"Refusing to overwrite: {args.output}")
seen = set()
kept = []
dropped = 0
for path in args.input:
with path.open(newline="", encoding="utf-8-sig") as f:
for row in csv.DictReader(f):
row["name"] = " ".join(row["name"].split()).title()
row["email"] = row["email"].strip().lower()
if not row["email"] or row["email"] in seen:
dropped += 1
continue
seen.add(row["email"])
kept.append(row)
with args.output.open("w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "email"], extrasaction="ignore")
writer.writeheader()
writer.writerows(kept)
print(f"Kept {len(kept)} rows, dropped {dropped} blank or duplicate rows.")
The duplicate rule is stated, not guessed: the first row with a given lower-cased email wins. The output is a new file, so the originals are untouched, and the newline="" argument is what the csv documentation asks for when opening files. Two cautions: .title() mangles names like “McDonald”, so drop that line if casing matters, and extrasaction="ignore" discards any other columns, so list every column you want to keep in fieldnames.
6. Generate a repeatable command-line report
Use it when you re-run the same summary every week with a different date range. argparse turns it into a tool with named options and automatic --help. This example totals an amount column by category for rows whose date (YYYY-MM-DD) falls in range.
import argparse
import csv
from collections import defaultdict
from datetime import date
from decimal import Decimal
from pathlib import Path
parser = argparse.ArgumentParser(description="Total amounts by category for a date range.")
parser.add_argument("input", type=Path)
parser.add_argument("--start", type=date.fromisoformat, required=True)
parser.add_argument("--end", type=date.fromisoformat, required=True)
parser.add_argument("--output", type=Path, help="write report here instead of printing")
args = parser.parse_args()
totals = defaultdict(Decimal)
with args.input.open(newline="", encoding="utf-8-sig") as f:
for row in csv.DictReader(f):
day = date.fromisoformat(row["date"])
if args.start <= day <= args.end:
totals[row["category"]] += Decimal(row["amount"])
lines = [f"{cat}: {total:.2f}" for cat, total in sorted(totals.items())]
report = "n".join(lines) or "No rows in range."
if args.output:
if args.output.exists():
raise SystemExit(f"Refusing to overwrite: {args.output}")
args.output.write_text(report + "n", encoding="utf-8")
else:
print(report)
Run it like this: python report.py sales.csv --start 2026-09-01 --end 2026-09-30. The input file is only read, never modified. A malformed date or amount raises an error naming the bad value, which is preferable to silently skipping rows.
7. Run a trusted external program and capture the result
Use it when an installed tool already does a step you need, such as Git, a converter, or a backup utility, and you want its output inside a script. Use subprocess.run with an argument list, not a single command string.
Best Value
import subprocess
import sys
repo = sys.argv[1] # e.g. /home/me/projects/site
try:
result = subprocess.run(
["git", "-C", repo, "status", "--short"],
capture_output=True,
text=True,
timeout=30,
check=True,
)
except FileNotFoundError:
raise SystemExit("git is not installed or not on PATH.")
except subprocess.TimeoutExpired:
raise SystemExit("git took longer than 30 seconds.")
except subprocess.CalledProcessError as err:
raise SystemExit(f"git failed ({err.returncode}): {err.stderr.strip()}")
print(result.stdout or "Working tree clean.")
The argument list means the path in repo is passed as a single argument and is not parsed by a shell, so spaces and special characters do not change the command’s meaning. Avoid shell=True unless you have a concrete need, and never combine it with text you did not write; Python’s subprocess documentation has a dedicated security section on this. Only run programs you trust, since the script inherits whatever that program does.
Choosing which to write first
| Script | Changes your files? | Main risk | Safeguard in the example |
|---|---|---|---|
| 1. Batch rename | Yes (renames) | Name collisions, hard to undo | Preview, skip if target exists |
| 2. Sort folder | Yes (moves) | Misfiled items, collisions | Preview, skip if target exists |
| 3. Dated backup | No (adds a copy) | Incomplete metadata, disk space | Timestamped destination, preview |
| 4. ZIP archive | No (adds an archive) | Deleting sources too early | Verification, never deletes |
| 5. CSV cleanup | No (new file) | Wrong duplicate rule, dropped columns | Writes a new file, reports counts |
| 6. Report | No (read-only) | Bad input data | Strict parsing, no overwrite |
| 7. External program | Depends on the tool | Shell injection, hangs | Argument list, timeout, error handling |
Start with 3 or 6: they cannot damage anything, so they are the safest way to get comfortable. Make 3 a habit before you run 1 or 2, because rename and move scripts are the ones that can scramble a folder.
Cross-platform notes
- Paths:
pathlibhandles/versusfor you; avoid building paths with string concatenation. - Case sensitivity: Linux file systems are usually case-sensitive; Windows and default macOS volumes usually are not. Extension matching in the examples lower-cases suffixes for that reason, except the
globpattern in script 1. - Running on a schedule: once a script is trustworthy, you can run it from your system’s scheduler. Do that only after you have used the preview mode on real data several times.
Where to go next
The argparse, shutil, zipfile, csv, and subprocess pages in the official Python documentation, and the standard-library tour in the official tutorial, cover the options these examples leave out. Automate the Boring Stuff with Python is a well-known longer learning path for the same kind of task, though this article did not verify the current edition or availability.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →




