Scripting Basics
10 examples to get you started with System Scripting - 7 basic and 3 intermediate.
Prerequisites
uv venv && source .venv/bin/activate
# stdlib only for most examples; typer optional:
uv pip install "typer>=0.15"- Python 3.14.0 - scripting examples use the standard library first.
Basic Examples
1. Script Entry Point
Guard side effects so imports stay safe.
def main() -> int:
print("running")
return 0
if __name__ == "__main__":
raise SystemExit(main())main()returns int exit code for cron and CI.- Importable modules should not run network calls at import time.
raise SystemExitavoidssys.exitin library-adjacent code.
Related: Robust Scripts - exit codes and logging
2. argparse CLI
Parse flags and positional args without extra dependencies.
import argparse
def main() -> int:
parser = argparse.ArgumentParser(description="Archive logs")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("path")
args = parser.parse_args()
print(args.path, args.dry_run)
return 0
if __name__ == "__main__":
raise SystemExit(main())action="store_true"for boolean flags.- Help text auto-generates from
ArgumentParser. - Validate paths after parse, not inside
add_argumentfor complex rules.
Related: File & Directory Automation - path handling
3. pathlib Paths
Object-oriented paths beat string concatenation.
from pathlib import Path
root = Path("data")
for csv in root.glob("**/*.csv"):
print(csv.resolve())/operator joins path segments portably.glob("**/*.csv")recurses with**(follow symlinks carefully).- Use
.read_text(encoding="utf-8")instead ofopen()when whole file fits in memory.
Related: File & Directory Automation - safe writes
4. Logging Instead of print
Structured logs survive cron email and log aggregators.
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("backup")
def main() -> int:
log.info("start backup")
return 0- Default log level INFO in prod scripts; DEBUG via
--verboseflag. - Log to stderr; keep stdout clean for JSON/pipe consumers.
- Include operation context in log messages, not giant data dumps.
Related: Robust Scripts - operational logging
5. Explicit Exit Codes
Callers distinguish failure modes via exit status.
EXIT_OK = 0
EXIT_USAGE = 2
EXIT_FAIL = 1
def main() -> int:
return EXIT_OK- Document codes in
--helpor README for operators. 2commonly signals usage errors (argparse uses this).- Never swallow exceptions without logging and non-zero exit.
Related: Robust Scripts - failure handling
6. Environment Variables
Read config from the environment for 12-factor scripts.
import os
API_URL = os.environ.get("API_URL", "https://api.example.com")
TOKEN = os.environ["API_TOKEN"] # required - raises KeyError if missing- Required secrets: fail fast with clear message if env var absent.
- Defaults only for non-secret dev convenience.
- Never print env vars containing tokens.
Related: Working with APIs & Webhooks - authenticated calls
7. Running Subcommands Safely
Delegate to external tools with explicit argument lists.
import subprocess
def run_git_status() -> int:
proc = subprocess.run(["git", "status", "--short"], check=False)
return proc.returncode- List form avoids shell injection - no
shell=Trueunless unavoidable. check=Falselets you map return codes yourself.- Capture output with
capture_output=Truewhen parsing results.
Related: subprocess & Shell Interop - capture and timeouts
Intermediate Examples
8. Typer CLI with Types
Typed CLI with less boilerplate than raw argparse.
import typer
app = typer.Typer()
@app.command()
def greet(name: str, loud: bool = False) -> None:
msg = f"hello {name}"
typer.echo(msg.upper() if loud else msg)
if __name__ == "__main__":
app()- Typer builds on Click - good for multi-command tools.
- Type hints drive option parsing and validation.
- Pair with
uv run script.pyinpyproject.tomlscripts table.
Related: Scheduling & Cron - scheduled typer CLIs
9. Config File + CLI Override
Merge defaults, file config, and CLI flags (last wins).
from dataclasses import dataclass
@dataclass
class Config:
retries: int = 3
dry_run: bool = False
def resolve_config(cli_retries: int | None, base: Config) -> Config:
if cli_retries is not None:
return Config(retries=cli_retries, dry_run=base.dry_run)
return base- Precedence: defaults < config file < environment < CLI.
- Immutable
Configdataclasses simplify testing. - Validate merged config before any side effects.
Related: Robust Scripts - dry-run mode
10. Idempotent Main Guard
Skip work when output already exists unless --force.
from pathlib import Path
def build_report(out: Path, force: bool) -> int:
if out.exists() and not force:
print(f"skip existing {out}")
return 0
out.write_text("report data\n", encoding="utf-8")
return 0- Idempotency makes cron reruns safe.
--forcedocuments intentional overwrite.- Atomic writes (temp file + rename) prevent half-written outputs - see file automation page.
Related: File & Directory Automation - atomic writes
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.