Not every problem needs a microservice. Sometimes you need a twenty-line script to rename files, sanity-check a CSV, or glue two internal APIs together until the “real” automation ships. Python is a strong choice because the standard library already covers files, dates, HTTP, and CSV, and packaging is optional for local tools.
This article collects practical patterns for scripts you will actually reuse, without turning each one into a framework project.
Start from a main guard
Use the familiar pattern so imports do not execute side effects:
def main() -> None:
...
if __name__ == "__main__":
main()
That matters the moment you add tests or import functions from the file in a REPL.
argparse for real CLIs
If the script takes more than one flag, reach for argparse instead of parsing sys.argv by hand. Subcommands map well to “verbs” (migrate, validate, report) when a script grows.
Keep help text human; your future self is the primary user.
Pathlib instead of string paths
pathlib.Path reads more clearly than os.path.join everywhere:
from pathlib import Path
root = Path("data") / "exports"
for csv_path in root.glob("*.csv"):
process(csv_path)
Use .resolve() when you need absolute paths for logging.
Safer file writes
For generated files, write to a temporary path and rename into place so interruptions half-way through do not leave corrupt outputs:
import tempfile
from pathlib import Path
def atomic_write(dest: Path, data: str) -> None:
dest.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile("w", delete=False, dir=dest.parent) as tmp:
tmp_path = Path(tmp.name)
tmp.write(data)
tmp_path.replace(dest)
Configuration: env vars and small TOML files
Combine defaults with environment overrides for secrets and deployment-specific values:
import os
API_BASE = os.environ.get("API_BASE", "https://api.example.com")
When configuration grows, a committed config.toml and a local override (git-ignored) keep teams aligned without baking secrets into the repo.
HTTP without extra dependencies
For internal tools, urllib.request can be enough for GETs and simple JSON posts. If you need retries, sessions, or complex TLS, add requests or httpx deliberately rather than by default — dependency weight matters when IT mirrors packages offline.
Logging instead of print
logging gives you levels and timestamps for free:
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
logging.info("Processed %s rows", count)
Reserve print for truly interactive CLIs.
One script, many environments
Make behavior explicit with flags (--dry-run) so a script that deletes or mutates data has a safe rehearsal mode. Fail fast when required env vars are missing rather than half-running.
When to promote a script
Signals that a script deserves packaging:
- Multiple people copy-pasted versions.
- It needs repeatable installs on CI runners.
- You want automatic tab completion or versioned releases.
Until then, a single file in tools/ with a short README stub is fine.
FAQ
Should I use type hints?
Yes for anything non-trivial; mypy catches whole classes of mistakes in automation.
What about async?
Use async when you wait on many I/O operations concurrently; otherwise synchronous code is simpler to read and debug.
How do I schedule these?
Cron, systemd timers, or your orchestrator’s CronJob — the script should exit with non-zero status on failure so operators notice.
Small Python utilities repay modest structure: clear entrypoints, safe file operations, and honest configuration. That is how “temporary” scripts survive long enough to earn better names.
Further Reading
Discover more articles on similar topics across our network
Comments
Loading comments…