Python automation cheat sheet
The whole path on one page, grouped by its five domains - scripting, files and data, OS and process, APIs and cloud, network and CI. Every output was printed by Python 3.14.4 on the lab's control node, and the rows are the ones that fail a script at 3 a.m. rather than the ones that appear most often.
- Python3.14.4
- Control nodeUbuntu 26.04 LTS
- Managed hostsRHEL 10.0
- requests2.34.2
- paramiko5.0.0
- pytest9.1.1
- PyYAML6.0.3
- boto3 / botocore1.43.78
- Commands41
- Reviewed24 August 2026
Domain 1: Scripting foundations (19%)
-
sys.executable · sys.version_infoWhich interpreter is actually running this. The first thing to print when a script works for you and not for cron.
bash Example session mkdir -p ~/auto/cs && cd ~/auto/cs && python3 -c "import sys; print(sys.executable); print(sys.version_info[:3])"/usr/bin/python3(3, 14, 4) -
ArgumentParser · required=True · action="count"A parsed Namespace. `-vv` counts to 2, which is how verbosity levels are done.
bash Example session cd ~/auto/cs && printf 'import argparse\np=argparse.ArgumentParser(prog="report")\np.add_argument("--host",required=True)\np.add_argument("-v","--verbose",action="count",default=0)\nprint(p.parse_args())\n' > a.py && python3 a.py --host web01 -vvNamespace(host='web01', verbose=2) -
a missing required argumentargparse prints the error and exits **2** by itself - you write no validation and no exit code.
bash Example session cd ~/auto/cs && python3 a.py 2>&1 | tail -1; echo "exit ${PIPESTATUS[0]}"report: error: the following arguments are required: --hostexit 2 -
sys.exit(3)Your own exit code, for a caller to branch on. 0 success, 1 general failure, and your own meanings above that.
bash Example session cd ~/auto/cs && python3 -c "import sys; sys.exit(3)"; echo "exit $?"exit 3 -
raise SystemExit('message')Prints to stderr and exits 1, with no traceback. The right shape for a configuration error.
bash Example session cd ~/auto/cs && python3 -c "raise SystemExit('config missing: LAB_TOKEN')"; echo "exit $?"config missing: LAB_TOKENexit 1 -
basicConfig(format=, datefmt=, stream=stderr) · exc_info=TrueTimestamped lines on stderr, `%d%%` formatted by logging rather than by you, and a traceback attached to the ERROR.
bash Example session mkdir -p ~/auto/cs && cd ~/auto/cs && python3 -c "import logging, syslogging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-7s %(message)s', datefmt='%H:%M:%S', stream=sys.stderr)logging.getLogger('urllib3').setLevel(logging.WARNING)log = logging.getLogger('report')log.info('started')log.warning('disk at %d%% on %s', 91, '/var')try: 1 / 0except ZeroDivisionError: log.error('the sum failed', exc_info=True)" 2>&1 | head -812:56:24 INFO started12:56:24 WARNING disk at 91% on /var12:56:24 ERROR the sum failedTraceback (most recent call last): File "<string>", line 9, in <module> 1 / 0 ~~^~~ZeroDivisionError: division by zero -
os.environ.get(name, default)A setting from the environment with a fallback, and no KeyError for the one that is absent.
bash Example session cd ~/auto/cs && LAB_MODE=strict python3 -c "import os; print(os.environ.get('LAB_MODE','default'), '|', os.environ.get('NOPE','default'))"strict | default -
PATH · HOME · LANGThe three that differ under a scheduler. Print them on the first failing run rather than guessing.
bash Example session cd ~/auto/cs && python3 -c "import sys; sys.exit(3)"; echo "exit $?"exit 3
Domain 2: Files, data formats and text (21%)
-
Path.write_text · read_text(encoding=) · stat().st_size · resolve()Always pass `encoding="utf-8"`. Without it the result depends on the machine.
bash Example session cd ~/auto/cs && python3 -c "import logging, syslogging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-7s %(message)s', datefmt='%H:%M:%S', stream=sys.stderr)logging.getLogger('urllib3').setLevel(logging.WARNING)logging.info('started'); logging.warning('disk at 91%%')"12:54:55 INFO started12:54:55 WARNING disk at 91%% -
Path.glob · .parent · .stem · .suffixSplitting a path without string surgery - `/var/log/app`, `run`, `.log`.
bash Example session cd ~/auto/cs && python3 -c "import osfor name in ('PATH','HOME','LANG'): print('%-5s %s' % (name, (os.environ.get(name) or '(unset)')[:48]))"PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/biHOME /home/sysadminLANG en_US.UTF-8 -
csv.DictReader · newline=""Rows as dicts, every value a **string** - the comparison needs int(). `newline=""` is required or quoted fields with newlines break.
bash Example session cd ~/auto/cs && printf 'host,pct\nweb01,50\ndb02,95\n' > d.csv && python3 -c "import csvwith open('d.csv', newline='', encoding='utf-8') as f: rows = list(csv.DictReader(f))print(rows)print([r['host'] for r in rows if int(r['pct']) >= 90])"[{'host': 'web01', 'pct': '50'}, {'host': 'db02', 'pct': '95'}]['db02'] -
json.dumps · loads · default=strTrue becomes `true` and None becomes `null`. A datetime needs `default=`, or it raises.
bash Example session cd ~/auto/cs && python3 -c "try: 1 / 0except ZeroDivisionError as exc: print(type(exc).__name__, '-', exc)finally: print('finally always runs')"ZeroDivisionError - division by zerofinally always runs -
yaml.safe_load · and two traps**`version: 1.10` parses as 1.1** and **`country: NO` as False.** Quote any value whose exact text matters.
bash Example session cd ~/auto/cs && printf 'hosts:\n - web01\n - db01\nport: 8080\n' > d.yml && python3 -c "import yamlprint(yaml.safe_load(open('d.yml', encoding='utf-8')))print(yaml.safe_load('version: 1.10'), yaml.safe_load('country: NO'))"{'hosts': ['web01', 'db01'], 'port': 8080}{'version': 1.1} {'country': False} -
named groups · groupdict() · findallOne compiled pattern with named groups gives you a dict, which is far easier to read than numbered groups.
bash Example session cd ~/auto/cs && python3 -c "from pathlib import Pathp = Path('report.txt')p.write_text('one\ntwo\n', encoding='utf-8')print(p.resolve()); print(p.stat().st_size, 'bytes'); print(p.read_text(encoding='utf-8').splitlines())"/home/sysadmin/auto/cs/report.txt8 bytes['one', 'two'] -
mkstemp + os.replaceAn atomic write: a reader sees the old file or the new one, never half of either. `os.replace` is the atomic step, and only on the same filesystem.
bash Example session cd ~/auto/cs && python3 -c "from pathlib import Pathprint(sorted(str(p) for p in Path('.').glob('*.py')))print(Path('/var/log/app/run.log').parent, '|', Path('/var/log/app/run.log').stem, '|', Path('/var/log/app/run.log').suffix)"['a.py']/var/log/app | run | .log -
make_archive · which · disk_usageA tarball in one call, a dependency check, and the free space to decide whether to try.
bash Example session cd ~/auto/cs && python3 -c "import jsonprint(json.dumps({'host': 'web01', 'up': True, 'load': None}))print(json.loads('{\"a\": [1, 2]}')['a'])print(json.dumps({'when': __import__('datetime').date(2026,8,24)}, default=str))"{"host": "web01", "up": true, "load": null}[1, 2]{"when": "2026-08-24"}
Domain 3: OS, process and task automation (20%)
-
run(argv, capture_output=True, text=True, timeout=, check=True)The one call worth memorising. A list so no shell parses it, a timeout so it cannot hang, and check so a failure is loud.
bash Example session cd ~/auto/cs && python3 -c "import reLINE = re.compile(r'^(?P<ip>\S+) .* \"(?P<method>[A-Z]+) (?P<path>\S+)[^\"]*\" (?P<status>\d{3})')m = LINE.match('10.0.0.1 - - [24/Aug/2026] \"GET /health HTTP/1.1\" 200')print(m.groupdict())print(re.findall(r'\d+', 'sda1 41922560 20961280'))"{'ip': '10.0.0.1', 'method': 'GET', 'path': '/health', 'status': '200'}['1', '41922560', '20961280'] -
returncode · stdout · stderrThe three things a finished command gives you. Read stderr before deciding what went wrong.
bash Example session cd ~/auto/cs && python3 -c "import os, tempfilefrom pathlib import Pathfinal = Path('atomic.txt')fd, tmp = tempfile.mkstemp(dir='.')with os.fdopen(fd, 'w', encoding='utf-8') as f: f.write('complete\n')os.replace(tmp, final)print(final.read_text(encoding='utf-8').strip(), '| mode', oct(final.stat().st_mode & 0o777))"complete | mode 0o600 -
CalledProcessError · TimeoutExpired`check=True` raises with the return code attached, and `timeout=` kills the child before raising.
bash Example session cd ~/auto/cs && python3 -c "import shutilfrom pathlib import PathPath('bundle').mkdir(exist_ok=True); Path('bundle/x.txt').write_text('x\n')print(shutil.make_archive('bundle', 'gztar', 'bundle'))print(shutil.which('tar'), '|', shutil.disk_usage('/').free // 1048576, 'MiB free')"/home/sysadmin/auto/cs/bundle.tar.gz/usr/bin/tar | 38424 MiB free -
env= replaces · dict(os.environ, X=y) adds**`env={"TOKEN":"abc"}` leaves the child with no HOME.** Copy the environment and add to it.
bash Example session cd ~/auto/cs && python3 -c "import subprocessr = subprocess.run(['uname', '-r'], capture_output=True, text=True, timeout=10, check=True)print(r.returncode, '|', r.stdout.strip(), '|', repr(r.stderr))"0 | 7.0.0-30-generic | '' -
cwd= per child · os.umask(0) to read it`cwd=` affects one child; `os.chdir` affects the whole process. Reading the umask means setting it and putting it back.
bash Example session cd ~/auto/cs && python3 -c "import subprocessr = subprocess.run(['sh', '-c', 'echo out; echo err >&2; exit 4'], capture_output=True, text=True)print('code', r.returncode, '| out', r.stdout.strip(), '| err', r.stderr.strip())"code 4 | out out | err err -
os.open(path, O_CREAT | O_EXCL, 0o600)A private file that was never anything else, and a second attempt that refuses. `write_text` then `chmod` has a window.
bash Example session cd ~/auto/cs && python3 -c "import subprocesstry: subprocess.run(['false'], check=True, timeout=10)except subprocess.CalledProcessError as exc: print(type(exc).__name__, 'returncode', exc.returncode)try: subprocess.run(['sleep', '30'], timeout=1)except subprocess.TimeoutExpired as exc: print(type(exc).__name__, 'after', exc.timeout, 's - child already killed')"CalledProcessError returncode 1TimeoutExpired after 1 s - child already killed -
fcntl.flock(f, LOCK_EX | LOCK_NB)A single-instance guard the kernel releases when the process dies - however it dies. An O_EXCL lock file goes stale; this cannot.
bash Example session cd ~/auto/cs && python3 -c "import os, subprocessprobe = ['sh', '-c', 'echo HOME=[\$HOME] TOKEN=[\$TOKEN]']print('replaced:', subprocess.run(probe, env={'TOKEN':'abc'}, capture_output=True, text=True).stdout.strip())print('copied :', subprocess.run(probe, env=dict(os.environ, TOKEN='abc'), capture_output=True, text=True).stdout.strip())"replaced: HOME=[] TOKEN=[abc]copied : HOME=[/home/sysadmin] TOKEN=[abc] -
Popen.returncode after a signalNegative in Python, `128 + n` in a shell. **137 is SIGKILL** - the OOM killer or a grace period expiring.
bash Example session cd ~/auto/cs && python3 -c "import os, subprocessprint('cwd=', subprocess.run(['pwd'], cwd='/tmp', capture_output=True, text=True).stdout.strip())print('parent unchanged:', os.getcwd())um = os.umask(0); os.umask(um)print('umask', oct(um), '-> a new file gets', oct(0o666 & ~um))"cwd= /tmpparent unchanged: /home/sysadmin/auto/csumask 0o2 -> a new file gets 0o664
Domain 4: APIs, web and cloud (20%)
-
requests.get(url, timeout=) · status_code · ok · .json()`ok` is `< 400`, not `== 200`. **There is no default timeout** - omit it and a hung server hangs the script.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestsr = requests.get('http://127.0.0.1:8000/hosts', timeout=5)print(r.status_code, r.reason, '| ok', r.ok, '| type', r.headers['Content-Type'])print('total', r.json()['total'], '| next_page', r.json()['next_page'])"200 OK | ok True | type application/jsontotal 5 | next_page 2 -
a 502 that is text/html`JSONDecodeError: Expecting value` is what a `<` looks like to a JSON parser. Check the status before you parse.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestsr = requests.get('http://127.0.0.1:8000/badgateway', timeout=5)print(r.status_code, r.headers['Content-Type'])try: r.json()except requests.exceptions.JSONDecodeError as exc: print(type(exc).__name__, '-', exc.msg, '- check the status before you parse')"502 text/htmlJSONDecodeError - Expecting value - check the status before you parse -
params= encodingSpaces and ampersands encoded, a list repeated, and `None` dropped entirely. Never build a query string by hand.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestsr = requests.get('http://127.0.0.1:8000/hosts', params={'page': 2, 'note': 'a b & c'}, timeout=5)print(r.url)print(requests.get('http://127.0.0.1:8000/hosts', params={'role': ['web','db'], 'x': None}, timeout=5).url)"http://127.0.0.1:8000/hosts?page=2¬e=a+b+%26+chttp://127.0.0.1:8000/hosts?role=web&role=db -
headers={'Authorization': 'Bearer ...'}401 without it, 200 with it. The token comes from the environment, never from the source, and never from `params=`.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestsprint(requests.get('http://127.0.0.1:8000/secure', timeout=5).status_code, '<- no header')r = requests.get('http://127.0.0.1:8000/secure', headers={'Authorization': 'Bearer let-me-in'}, timeout=5)print(r.status_code, r.json())"401 <- no header200 {'secret': 'the build is green'} -
json= sets the body and the Content-Type201 and a Location header. `data=` with a dict would send a **form** and get a 400 from a JSON API.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestsr = requests.post('http://127.0.0.1:8000/reports', json={'host': 'web01', 'ok': True}, timeout=5)print(r.status_code, r.headers['Location'], '|', r.request.headers['Content-Type'])print('sent:', r.request.body)"201 /reports/1 | application/jsonsent: b'{"host": "web01", "ok": true}' -
follow next_page, on one SessionFive hosts over three pages and **one TCP connection**. Loop on the server's pointer, not on arithmetic of your own.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import requestswith requests.Session() as s: requests.get('http://127.0.0.1:8000/reset', timeout=5) page, names = 1, [] while page is not None: body = s.get('http://127.0.0.1:8000/hosts', params={'page': page}, timeout=5).json() names += [h['name'] for h in body['hosts']]; page = body['next_page']print(len(names), 'hosts over', requests.get('http://127.0.0.1:8000/stats', timeout=5).json()['connections'], 'connection(s)')"5 hosts over 1 connection(s) -
ReadTimeout · Retry-AfterA timeout on every call, and when a 503 tells you how long to wait, wait that long rather than guessing.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import time, requestsrequests.get('http://127.0.0.1:8000/reset', timeout=5)try: requests.get('http://127.0.0.1:8000/slow', timeout=1)except requests.exceptions.ReadTimeout as exc: print(type(exc).__name__, '- requests has NO default timeout')r = requests.get('http://127.0.0.1:8000/flaky', timeout=5)print(r.status_code, 'Retry-After', r.headers.get('Retry-After'), '-> honour it, do not guess')"ReadTimeout - requests has NO default timeout503 Retry-After 1 -> honour it, do not guess -
NoRegionError · and S3 saying nothing**ec2 raises; s3 silently uses us-east-1.** The silent one is why your bucket "does not exist". Set the region explicitly.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import boto3, botocore.exceptionstry: boto3.client('ec2', aws_access_key_id='stub', aws_secret_access_key='stub')except botocore.exceptions.NoRegionError as exc: print('ec2:', type(exc).__name__, '-', exc)s3 = boto3.client('s3', aws_access_key_id='stub', aws_secret_access_key='stub')print('s3 : no error at all ->', s3.meta.region_name, s3.meta.endpoint_url)"ec2: NoRegionError - You must specify a region.s3 : no error at all -> us-east-1 https://s3.amazonaws.com
Domain 5: Network, testing and CI/CD (20%)
-
connect · exec_command · recv_exit_statusparamiko raises **nothing** for a failing command. Read the exit status or report success for a failure.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import paramikoc = paramiko.SSHClient(); c.load_system_host_keys()c.connect('192.168.0.31', username='sysadmin', timeout=10)_, out, err = c.exec_command('hostname -s; exit 0')print(out.read().decode().strip(), '| exit', out.channel.recv_exit_status())c.close()"rhcsa-a01 | exit 0 -
RejectPolicy, which is the default"Server not found in known_hosts" is the library protecting you. `AutoAddPolicy` is `StrictHostKeyChecking=no`.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import paramikoc = paramiko.SSHClient(); c.load_host_keys('/dev/null')try: c.connect('192.168.0.31', username='sysadmin', timeout=10)except paramiko.SSHException as exc: print(type(exc).__name__, '-', exc, '<- RejectPolicy is the default, and correct')c.close()"SSHException - Server '192.168.0.31' not found in known_hosts <- RejectPolicy is the default, and correct -
put to .part, then posix_renameA reader never sees a half-written upload. `rename` fails over an existing target; `posix_rename` overwrites.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import paramikoc = paramiko.SSHClient(); c.load_system_host_keys(); c.connect('192.168.0.31', username='sysadmin', timeout=10)sftp = c.open_sftp()sftp.put('report.txt', '/home/sysadmin/cs-report.txt.part', confirm=True)sftp.posix_rename('/home/sysadmin/cs-report.txt.part', '/home/sysadmin/cs-report.txt')print('landed', sftp.stat('/home/sysadmin/cs-report.txt').st_size, 'bytes - via .part then posix_rename')sftp.remove('/home/sysadmin/cs-report.txt'); sftp.close(); c.close()"landed 8 bytes - via .part then posix_rename -
ThreadPoolExecutor over an inventoryOne dark host costs the whole run one timeout instead of holding up every other host. Catch per host and keep a result for each.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -c "import concurrent.futures, time, paramikoHOSTS = {'a01': '192.168.0.31', 'b01': '192.168.0.33', 'dark': '10.255.255.1'}def probe(item): name, addr = item c = paramiko.SSHClient(); c.load_system_host_keys(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: c.connect(addr, username='sysadmin', timeout=3, banner_timeout=3, auth_timeout=3) return name, 'ok' except (paramiko.SSHException, OSError) as exc: return name, type(exc).__name__ finally: c.close()start = time.monotonic()with concurrent.futures.ThreadPoolExecutor(max_workers=8) as pool: for name, state in sorted(pool.map(probe, HOSTS.items())): print(' %-5s %s' % (name, state))print('%.1fs for %d hosts, one of them dark' % (time.monotonic() - start, len(HOSTS)))" a01 ok b01 ok dark TimeoutError3.0s for 3 hosts, one of them dark -
pytest -q · @pytest.mark.parametrizeThree cases, three tests, three separate results. A loop inside one test stops at the first failure.
bash Example session cd ~/auto/cs && printf 'def double(n):\n return n * 2\n' > m.py && printf 'import pytest\nfrom m import double\n@pytest.mark.parametrize("n,want", [(2,4),(0,0),(-3,-6)])\ndef test_double(n, want):\n assert double(n) == want\n' > test_m.py && ~/autoenv/bin/python -m pytest -q... [100%]3 passed in 0.00s -
a failing assert, and --tb=line`assert 6 == 7` - pytest rewrites the assertion so both values are in the message. Write no message.
bash Example session cd ~/auto/cs && printf 'from m import double\ndef test_wrong():\n assert double(3) == 7\n' > test_bad.py && ~/autoenv/bin/python -m pytest test_bad.py -q --tb=line 2>&1 | tail -4; echo "exit ${PIPESTATUS[0]}"/home/sysadmin/auto/cs/test_bad.py:3: assert 6 == 7=========================== short test summary info ============================FAILED test_bad.py::test_wrong - assert 6 == 71 failed in 0.00sexit 1 -
pytest exit 5**No tests collected exits 5**, not 0. A CI step that only treats 1 as failure goes green having run nothing.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -m pytest -q -k nothing_matches_this 2>&1 | tail -2; echo "exit 5 means no tests collected: ${PIPESTATUS[0]}" 4 deselected in 0.00sexit 5 means no tests collected: 5 -
patch("thismodule.subprocess.run", return_value=CompletedProcess(...))Patch where the name is **looked up**, not where it is defined - and assert on the argv that was passed, not just the result.
bash Example session cd ~/auto/cs && ~/autoenv/bin/python -m pytest test_mock.py -q. [100%]1 passed in 0.00s -
ruff check --select S,PLW,E722Five findings in one second: shell=True, no check, no timeout, a bare except. The cheapest gate that exists.
bash Example session cd ~/auto/cs && printf 'import subprocess, requests\nsubprocess.run("ls " + input(), shell=True)\nrequests.get("http://x")\ntry:\n pass\nexcept:\n pass\n' > bad.py && ~/autoenv/bin/ruff check --no-cache --select S,PLW,E722 --output-format=concise bad.py; echo "ruff exit $?"bad.py:2:1: S602 `subprocess` call with `shell=True` identified, security issuebad.py:2:1: PLW1510 `subprocess.run` without explicit `check` argumentbad.py:3:1: S113 Probable use of `requests` call without timeoutbad.py:6:1: E722 Do not use bare `except`bad.py:6:1: S110 `try`-`except`-`pass` detected, consider logging the exceptionFound 5 errors.ruff exit 1
No command matches that search.