Python for DevOps Cheat Sheet
Scannable Python patterns for the Python for DevOps track. Prefer the full tutorials when you need why, not only how.
Python syntax
x: int = 1
name = "payments"
f"{name}-{x}"
a, b = 1, 2
# truthiness: empty str/list/dict/None are False
| Construct | Example |
if / elif / else | if code == 0: ... |
match | match status: case 200: ... |
for | for line in path.open(): ... |
while | while retries: ... |
| Comprehension | [x for x in items if x] |
| Generator | (parse(l) for l in lines) |
| f-string | f"region={region}" |
| walrus | if (m := pat.search(line)): |
Data structures
| Type | Create | Notes |
| list | [1, 2] | ordered, mutable |
| tuple | (1, 2) | immutable |
| dict | {"a": 1} | .get(k, default) |
| set | {1, 2} | unique, unordered |
collections.Counter | Counter(levels) | tallies |
dataclasses.dataclass | @dataclass | structured records |
hosts = {"api": "10.0.0.1", "db": "10.0.0.2"}
ids = [h for h in hosts if h.startswith("a")]
File handling
from pathlib import Path
import json, csv
import yaml # PyYAML — safe_load only
p = Path("logs") / "app.log"
text = p.read_text(encoding="utf-8")
for line in p.open(encoding="utf-8"):
...
data = json.loads(text)
Path("out.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
cfg = yaml.safe_load(Path("deploy.yaml").read_text(encoding="utf-8"))
| Task | API |
| Join paths | Path("a") / "b" |
| Glob | Path(".").glob("**/*.yaml") |
| CSV | csv.DictReader / DictWriter |
| Temp | tempfile.TemporaryDirectory |
| Copy/move | shutil.copy2, shutil.move |
Exceptions
try:
cfg = load(path)
except FileNotFoundError as exc:
raise SystemExit(f"missing: {exc}") from exc
except (json.JSONDecodeError, ValueError) as exc:
print(exc, file=sys.stderr)
raise SystemExit(2) from exc
finally:
cleanup()
| Pattern | Use |
raise X from e | preserve cause |
| Custom exception | class ConfigError(Exception): ... |
| Exit taxonomy | 0 ok · 2 usage/validation · 1 runtime failure |
Logging
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger("rebash.ops")
log.info("inventory_start region=%s", region) # never log tokens
| Level | When |
| DEBUG | local diagnosis |
| INFO | normal ops events |
| WARNING | degraded but continuing |
| ERROR | failed action |
| CRITICAL | stop the world |
requests / httpx
import httpx
with httpx.Client(timeout=10.0) as client:
r = client.get(url, headers={"Authorization": f"Bearer {token}"})
r.raise_for_status()
payload = r.json()
| Rule | Detail |
| Always set timeouts | connect + read |
| Retries | idempotent GETs with backoff |
| Auth | env / secret store — not source |
| Pagination | follow Link / next tokens explicitly |
argparse
import argparse
p = argparse.ArgumentParser(description="Inventory hosts")
p.add_argument("--region", required=True)
p.add_argument("--json", action="store_true")
p.add_argument("--dry-run", action="store_true", default=True)
args = p.parse_args()
Click
import click
@click.command()
@click.option("--region", required=True)
@click.option("--apply", is_flag=True, default=False)
def main(region: str, apply: bool) -> None:
click.echo(region)
import typer
app = typer.Typer()
@app.command()
def collect(fixture: Path | None = None) -> None:
...
Docker SDK
import docker
client = docker.from_env()
# Prefer report-first:
for img in client.images.list(filters={"dangling": True}):
print(img.short_id, img.attrs.get("Size"))
# Delete only behind --apply
| Safety | Practice |
| Default dry-run | require --apply |
| Daemon down | --fixture path for labs/CI |
| Labels | filter by label=... before prune |
Kubernetes client
from kubernetes import client, config
config.load_kube_config() # or load_incluster_config()
v1 = client.CoreV1Api()
pods = v1.list_namespaced_pod("payments")
for pod in pods.items:
print(pod.metadata.name, pod.status.phase)
| Safety | Practice |
| Read-only first | list/get before patch/delete |
| Namespace scope | never cluster-admin for scripts |
| Offline | fixture JSON for CI |
boto3
import boto3
ec2 = boto3.client("ec2", region_name="eu-west-2")
resp = ec2.describe_instances()
# Prefer instance roles / OIDC — never hard-code keys
| Pattern | Note |
| Sessions | boto3.Session(profile_name=...) |
| Paginators | ec2.get_paginator("describe_instances") |
| Errors | catch ClientError; map to exit codes |
| Labs/CI | fixture JSON when credentials absent |
pytest
import pytest
@pytest.fixture
def sample_log(tmp_path):
p = tmp_path / "a.log"
p.write_text("2026-07-28T10:00:00Z ERROR msg=\"x\"\n", encoding="utf-8")
return p
def test_counts(sample_log):
assert analyse(sample_log)["levels"]["ERROR"] == 1
| Tip | Detail |
| Fixtures | tmp_path, monkeypatch |
| Mock HTTP | respx / unittest.mock |
| No live cloud | gate integration with env marker |
| CLI tests | CliRunner (Click/Typer) |
| Area | Commands / notes |
| venv | python3 -m venv .venv · source .venv/bin/activate |
| pip | pip install -r requirements.txt · pin versions |
| uv | uv venv · uv sync · uv run pytest |
| subprocess | subprocess.run([...], check=True, text=True) · list args |
| env/secrets | os.environ["KEY"] · never commit .env |
| pyproject | [project.scripts] · pip install -e . |
Common mistakes
subprocess.run("cmd $var", shell=True) — injection risk; use list args yaml.load without a SafeLoader — prefer safe_load - Missing HTTP timeouts — hung jobs and stuck pipelines
- Logging tokens or printing secrets in CI
- Shipping destructive Docker/K8s/cloud actions without a dry-run default
- Hard-coded cloud keys instead of roles / fixtures for tests