Useful Python Scripts for Automating Boring Dev Work

Stackademic

Copy-ready Python scripts for renaming files, merging CSVs, health checks, log redaction, and more—patterns that save hours without building a full app.

Useful Python scripts are the fastest way to pay down repetitive work: renaming files, cleaning CSV exports, pinging APIs, or generating reports. You do not need a framework — a twenty-line file you run from the terminal often beats a half-finished internal tool.

This article walks through patterns you can copy, adapt, and keep in a personal scripts/ folder. Every example uses the standard library or packages you likely already have (requests, pandas optional).

How to structure scripts you will actually reuse

Pick a consistent layout:

scripts/
  README.md          # one line per script
  _config.example.toml
  rename_photos.py
  merge_csvs.py

Conventions that save future-you:

  • Use argparse for anything with more than one hard-coded path.
  • Print what would happen with --dry-run before destructive operations.
  • Log to stderr; reserve stdout for machine-readable output (JSON lines) when piping.
import argparse
import sys

def main() -> int:
    parser = argparse.ArgumentParser(description="Example script skeleton")
    parser.add_argument("input_path")
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    # ...
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

Script 1: Bulk rename with predictable patterns

Renaming screenshot dumps is a classic time sink.

from pathlib import Path
import re

def rename_screenshots(folder: Path, prefix: str = "shot") -> None:
    pattern = re.compile(r"^Screen Shot.*\.png$", re.I)
    counter = 1
    for path in sorted(folder.glob("*.png")):
        if not pattern.match(path.name):
            continue
        target = folder / f"{prefix}_{counter:03d}.png"
        if target.exists():
            raise FileExistsError(target)
        path.rename(target)
        counter += 1

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

Caveat: Test on a copy first. On case-insensitive filesystems, renaming only changes case in two steps.

Script 2: Merge CSV exports without Excel

Analytics tools love exporting one CSV per day. Combine them:

import csv
from pathlib import Path

def merge_csv(directory: Path, output: Path) -> None:
    files = sorted(directory.glob("*.csv"))
    if not files:
        raise SystemExit("no csv files found")
    with output.open("w", newline="") as out_f:
        writer = None
        for i, path in enumerate(files):
            with path.open(newline="") as in_f:
                reader = csv.DictReader(in_f)
                if writer is None:
                    writer = csv.DictWriter(out_f, fieldnames=reader.fieldnames)
                    writer.writeheader()
                for row in reader:
                    writer.writerow(row)
    print(f"wrote {output}")

if __name__ == "__main__":
    merge_csv(Path("exports"), Path("merged.csv"))

If files have mismatched columns, switch to pandas concat with join="outer" and inspect null rates before trusting the merge.

Script 3: Watch a folder and run a command

Automate lightweight builds or virus scans when files land in a directory. The standard library watchdog package is popular; here is a polling version with zero dependencies:

import hashlib
import subprocess
import time
from pathlib import Path

def fingerprint(folder: Path) -> str:
    h = hashlib.sha256()
    for path in sorted(folder.rglob("*")):
        if path.is_file():
            h.update(path.name.encode())
            h.update(str(path.stat().st_mtime_ns).encode())
    return h.hexdigest()

def watch(folder: Path, command: list[str], interval: float = 2.0) -> None:
    last = fingerprint(folder)
    while True:
        time.sleep(interval)
        current = fingerprint(folder)
        if current != last:
            subprocess.run(command, check=False)
            last = current

if __name__ == "__main__":
    watch(Path("./inbox"), ["python", "process_inbox.py"])

Polling wastes CPU on large trees; for production file triggers, use watchdog or OS-level hooks.

Script 4: HTTP health check with timeouts

Quick uptime scripts belong in cron or GitHub Actions.

import sys
import urllib.request

URLS = [
    "https://api.example.com/health",
    "https://www.example.com/",
]

def check(url: str, timeout: float = 5.0) -> bool:
    req = urllib.request.Request(url, method="GET")
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return 200 <= resp.status < 300
    except Exception as exc:
        print(f"FAIL {url}: {exc}", file=sys.stderr)
        return False

if __name__ == "__main__":
    ok = all(check(u) for u in URLS)
    raise SystemExit(0 if ok else 1)

Return a non-zero exit code so monitoring tools alert correctly.

Script 5: Strip secrets before sharing logs

Before pasting logs into tickets, redact obvious tokens:

import re
import sys

PATTERNS = [
    (re.compile(r"Bearer\s+[A-Za-z0-9\-_.]+"), "Bearer [REDACTED]"),
    (re.compile(r"api[_-]?key[\"']?\s*[:=]\s*[\"']?[\w-]+", re.I), "api_key=[REDACTED]"),
]

def redact(text: str) -> str:
    for pattern, replacement in PATTERNS:
        text = pattern.sub(replacement, text)
    return text

if __name__ == "__main__":
    print(redact(sys.stdin.read()))

Maintain project-specific patterns (AWS keys, JWT blobs) in a local file you do not commit.

Script 6: Generate a markdown changelog from git

Useful before releases:

import subprocess

def git_log(since_tag: str) -> str:
    cmd = [
        "git", "log",
        f"{since_tag}..HEAD",
        "--pretty=format:- %s (%h)",
    ]
    return subprocess.check_output(cmd, text=True)

if __name__ == "__main__":
    print("# Changes since v1.2.0\n")
    print(git_log("v1.2.0"))

Pair with conventional commits if you want automated grouping by type.

Script 7: Resize images for docs

Documentation repos balloon when people paste 4K PNGs. Pillow makes batch resize simple:

from pathlib import Path
from PIL import Image

def resize_images(folder: Path, max_width: int = 1200) -> None:
    for path in folder.glob("**/*"):
        if path.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp"}:
            continue
        with Image.open(path) as img:
            if img.width <= max_width:
                continue
            ratio = max_width / img.width
            size = (max_width, int(img.height * ratio))
            img.resize(size, Image.Resampling.LANCZOS).save(path, optimize=True)

if __name__ == "__main__":
    resize_images(Path("docs/images"))

Always keep originals elsewhere if you might need print-quality assets.

Packaging and sharing scripts across a team

When a script graduates from personal use:

  1. Add it to a small internal package with pyproject.toml.
  2. Pin dependencies with uv or pip-tools.
  3. Document required environment variables in .env.example.
  4. Run ruff and mypy if the script touches production data.

For one-offs, a gist plus README is fine. Do not prematurely containerize a forty-line utility.

Common pitfalls

  • Hard-coded paths — break on every new laptop. Use Path.home() or env vars.
  • Silent failures — wrap external calls, print exceptions, exit non-zero.
  • Timezone bugs — use datetime.timezone.utc when parsing API timestamps.
  • Unicode on Windows — set PYTHONUTF8=1 or configure console encoding for international filenames.

FAQ

Are Jupyter notebooks good for scripts?
Notebooks excel for exploration; convert stable flows to .py files with functions and tests.

Should I use click instead of argparse?
click is ergonomic for multi-command CLIs. Stick with argparse for single-purpose tools to avoid extra dependencies.

How do I schedule these on Linux?
Use cron or systemd timers. On macOS, launchd plist entries work well. Log output to rotated files.

Script 8: Diff two JSON config files

Deployments often fail because a staging JSON config drifted from production. A quick diff script avoids eyeballing thousand-line files:

import json
import sys
from pathlib import Path

def load(path: Path) -> dict:
    return json.loads(path.read_text())

def deep_diff(a, b, prefix="") -> list[str]:
    changes: list[str] = []
    if isinstance(a, dict) and isinstance(b, dict):
        keys = set(a) | set(b)
        for key in sorted(keys):
            p = f"{prefix}.{key}" if prefix else key
            if key not in a:
                changes.append(f"+ {p}: {b[key]!r}")
            elif key not in b:
                changes.append(f"- {p}: {a[key]!r}")
            else:
                changes.extend(deep_diff(a[key], b[key], p))
    elif a != b:
        changes.append(f"~ {prefix}: {a!r} -> {b!r}")
    return changes

if __name__ == "__main__":
    left, right = map(Path, sys.argv[1:3])
    for line in deep_diff(load(left), load(right)):
        print(line)

Pair with pre-commit hooks so configs cannot merge when critical keys change without review.

Script 9: Archive old logs with compression

Disk fills up when services log verbosely. Roll logs older than N days into tar.gz archives:

import tarfile
from datetime import datetime, timedelta, timezone
from pathlib import Path

def archive_logs(log_dir: Path, older_than_days: int = 14) -> None:
    cutoff = datetime.now(timezone.utc) - timedelta(days=older_than_days)
    targets = [
        p for p in log_dir.glob("*.log")
        if datetime.fromtimestamp(p.stat().st_mtime, tz=timezone.utc) < cutoff
    ]
    if not targets:
        return
    stamp = datetime.now(timezone.utc).strftime("%Y%m%d")
    archive = log_dir / f"logs-{stamp}.tar.gz"
    with tarfile.open(archive, "w:gz") as tar:
        for path in targets:
            tar.add(path, arcname=path.name)
            path.unlink()

if __name__ == "__main__":
    archive_logs(Path("/var/log/myapp"))

Verify restore procedures occasionally — archives nobody can read are not backups.

Where to go next

Build a habit: every time you do something twice in a spreadsheet, write a script instead of the third manual pass. Keep them small, versioned, and boring — boring scripts are the ones teammates trust enough to run on real data.