Frontend Labs
⋯

38. Python as a better shell: automation and a systemd service

38.1 When a shell script should become Python

Bash is excellent for 10-line glue. Switch to Python when the script needs data structures, error handling beyond set -e, JSON or HTTP, tests, or when it is longer than a screen. A typical operations script:

#!/usr/bin/env python3
# labs/cleanup.py: delete old files under a directory, safely, with a dry run
import argparse
import logging
import sys
import time
from pathlib import Path

log = logging.getLogger("cleanup")

def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser()
    p.add_argument("root", type=Path)
    p.add_argument("--days", type=float, default=14)
    p.add_argument("--glob", default="*.log*")
    p.add_argument("--apply", action="store_true", help="actually delete (default: dry run)")
    a = p.parse_args(argv)
    logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")

    root = a.root.resolve()
    if root == Path("/") or len(root.parts) < 3:
        log.error("refusing to operate on %s", root)       # guard against catastrophic paths
        return 2
    cutoff = time.time() - a.days * 86400
    freed = 0
    for f in root.rglob(a.glob):
        if f.is_symlink() or not f.is_file():
            continue                                       # never follow symlinks out of root
        st = f.stat()
        if st.st_mtime < cutoff:
            freed += st.st_size
            log.info("%s %s (%.1f MB)", "delete" if a.apply else "would delete", f, st.st_size / 2**20)
            if a.apply:
                f.unlink(missing_ok=True)
    log.info("total %.1f MB %s", freed / 2**20, "freed" if a.apply else "reclaimable")
    return 0

if __name__ == "__main__":
    sys.exit(main())

The habits in that script are the point: dry run by default, refuse obviously dangerous targets, do not follow symlinks (a symlink to /etc inside a log directory is a classic privilege-escalation path for root cron jobs), log what you do, and return meaningful exit codes.

38.2 Run it on a schedule with a systemd timer

# /etc/systemd/system/cleanup.service
[Unit]
Description=Delete old application logs

[Service]
Type=oneshot
User=appsvc
ExecStart=/opt/tools/.venv/bin/python /opt/tools/cleanup.py /var/log/myapp --days 14 --apply
# Sandboxing (OS Ch 67): the script can only write where it must.
ProtectSystem=strict
ReadWritePaths=/var/log/myapp
PrivateTmp=yes
NoNewPrivileges=yes
# /etc/systemd/system/cleanup.timer
[Timer]
OnCalendar=daily
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now cleanup.timer
systemctl list-timers cleanup.timer
journalctl -u cleanup.service --since today       # stdout/stderr land here

Point ExecStart at the virtual environment's interpreter, not /usr/bin/python3, so the service gets exactly the dependencies you installed. OS Ch 67 explains each sandboxing option.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36