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
argparsefor anything with more than one hard-coded path. - Print what would happen with
--dry-runbefore 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:
- Add it to a small internal package with
pyproject.toml. - Pin dependencies with
uvorpip-tools. - Document required environment variables in
.env.example. - Run
ruffandmypyif 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.utcwhen parsing API timestamps. - Unicode on Windows — set
PYTHONUTF8=1or 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.
Comments
Loading comments…