The first six days got you a script that runs. Today we close out Week 1 with the three things that turn a script into a tool you’d actually trust against production gear: reading and writing files without leaking handles, catching errors without silently swallowing them, and using the logging module so your runs leave a clear paper trail. By the end of today you’ll have a small script that reads a device list from a file, logs per‑device outcomes to both console and a log file, and exits cleanly when something explodes.
This is Day 7 of the 21‑post Python for Network Engineers series — the last day of foundations before we dig into network‑specific libraries next week.
Reading files with with open(...)
with open("hosts.txt") as f:
for line in f:
print(line.rstrip()) # rstrip removes the trailing \n
The with statement is a context manager. It guarantees the file is closed when the block exits — even if an exception fires partway through. Always use it. The old f = open(...); f.close() pattern leaks file handles when something goes wrong in between, and on long‑running scripts that bites.
Iterating the file object directly (for line in f) is the right way to read large files — it streams one line at a time instead of loading the whole file into memory. f.read() reads the entire file as one string; f.readlines() returns a list of lines. Use read() for small config snippets, the iterator for log files.
Writing files
with open("backup.cfg", "w") as f: # "w" overwrites, "a" appends
f.write("hostname core-sw01\n")
f.write("interface Gi0/1\n description Uplink\n")
# Or write a list of lines in one call:
config_lines = ["vlan 10", "vlan 20", "vlan 30"]
with open("vlans.cfg", "w") as f:
f.write("\n".join(config_lines) + "\n")
Modes you’ll use: "r" read (default), "w" write (truncates), "a" append, "rb"/"wb" for binary. Add encoding="utf-8" on Windows if you’re writing anything other than pure ASCII to avoid surprises.
pathlib — the modern way to handle paths
Stop concatenating path strings with / by hand. The pathlib module gives you Path objects that handle Windows vs. Linux separators, file existence, and a clean API:
from pathlib import Path
backup_dir = Path("backups")
backup_dir.mkdir(exist_ok=True) # like mkdir -p
target = backup_dir / "core-sw01.cfg" # joins with the right separator
target.write_text("hostname core-sw01\n") # one-liner write
print(target.exists(), target.stat().st_size)
Path objects play nicely with open(...) too — open(target) works without conversion. For new code, default to pathlib over os.path.
Exceptions — catch what you mean
Things go wrong: a file doesn’t exist, a device times out, an API returns a 401, a key is missing. Python’s mechanism for those is the exception. The wrong way to handle them:
# Don't do this — swallows everything, including bugs you'd want to see
try:
do_stuff()
except:
pass
The right way:
try:
with open("hosts.txt") as f:
hosts = [line.strip() for line in f if line.strip()]
except FileNotFoundError:
print("hosts.txt missing — create it with one IP per line")
sys.exit(1)
except PermissionError as e:
print(f"can't read hosts.txt: {e}")
sys.exit(1)
Catch specific exception types. Let unexpected ones bubble up with a real traceback so you can fix them. The exception types you’ll see most in network code:
| Exception | When it fires |
|---|---|
FileNotFoundError |
Opening a path that doesn’t exist |
PermissionError |
Reading/writing a file you don’t have access to |
KeyError |
Indexing a dict with a missing key |
ValueError |
A function got a value it can’t handle (e.g. int("abc")) |
TypeError |
Wrong type (e.g. "x" + 1) |
TimeoutError |
SSH/socket call ran out of time |
ConnectionError |
Network‑level failure |
The full try / except / else / finally shape:
try:
n = int(user_input)
except ValueError:
print("not a number")
else:
print(f"got {n}") # runs only if try succeeded
finally:
print("always runs") # cleanup, runs whether or not an exception fired
Most production code uses try / except. finally earns its keep when you must release a resource (close a session, drop a lock); else is rarer but useful when you want “do this only on success” without burying it in the try.
Raise your own exceptions
def parse_vlan(text: str) -> int:
try:
v = int(text)
except ValueError as e:
raise ValueError(f"VLAN must be an integer, got {text!r}") from e
if not 1 <= v <= 4094:
raise ValueError(f"VLAN {v} out of range 1..4094")
return v
raise ... from e chains the original exception so the traceback shows both — invaluable when you’re wrapping someone else’s library and want to add context without losing the root cause.
Logging — print() doesn’t scale
print() is fine in a script you’ll run once. The moment you want to:
- see only WARNING and above on the console, but DEBUG in a file,
- add timestamps without writing them yourself,
- rotate log files so they don’t fill the disk,
- or have helpers in your modules log without each one knowing where the log goes,
…you want the standard library’s logging module. Minimal sane setup:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[
logging.FileHandler("backup.log"),
logging.StreamHandler(), # console
],
)
log = logging.getLogger("backup")
log.info("starting backup run")
log.warning("device timeout: 10.0.0.1")
log.error("backup failed: %s", "permission denied")
log.debug("this won't show — level is INFO")
Levels in increasing severity: DEBUG, INFO, WARNING, ERROR, CRITICAL. Set the level once, use the right level when you log, and you can later switch from “show me everything” to “show me only failures” by changing one line.
Idiom: always pass log arguments as separate parameters (log.error("failed: %s", err)) rather than using f‑strings (log.error(f"failed: {err}")). The first form skips the string formatting entirely if the level is filtered out — a real win in tight loops.
Putting it all together
A script that reads a device list, “processes” each one with a fake function that occasionally fails, logs per‑device outcomes to both console and file, and exits with the right status code:
"""day7_backup.py — read hosts.txt, "back up" each one, log results."""
import logging
import random
import sys
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
handlers=[logging.FileHandler("backup.log"), logging.StreamHandler()],
)
log = logging.getLogger("backup")
def fake_backup(host: str) -> str:
"""Pretend to fetch a config. Fails ~20% of the time."""
if random.random() < 0.2:
raise ConnectionError(f"timeout connecting to {host}")
return f"hostname {host}\ninterface Gi0/0\n no shutdown\n"
def main():
hosts_file = Path("hosts.txt")
try:
hosts = [h.strip() for h in hosts_file.read_text().splitlines() if h.strip()]
except FileNotFoundError:
log.error("%s not found — create it with one IP per line", hosts_file)
sys.exit(1)
backup_dir = Path("backups")
backup_dir.mkdir(exist_ok=True)
ok = fail = 0
for host in hosts:
try:
cfg = fake_backup(host)
except ConnectionError as e:
log.warning("%s: %s", host, e)
fail += 1
continue
(backup_dir / f"{host}.cfg").write_text(cfg)
log.info("%s: backed up", host)
ok += 1
log.info("done: %d ok, %d failed", ok, fail)
sys.exit(0 if fail == 0 else 2)
if __name__ == "__main__":
main()
Drop a hosts.txt with a few IPs in it, run python day7_backup.py, and you’ll see timestamped per‑device results on the console and in backup.log, plus saved config files in ./backups/. Replace fake_backup with a real Netmiko call on Day 13 and this becomes your first usable backup tool.
Exercises
- Write a snippet that reads
hosts.txt(you may need to create it first with a few IPs, one per line) and prints each non‑blank, stripped line. Usewith openand aforloop. - Use
pathlibto: create a directory./outif it doesn’t exist, then write the string"test\n"to./out/result.txt. Then printTrueorFalsefor whether the file exists. - Wrap
int(input("VLAN? "))in atry / exceptthat catchesValueError. On bad input, print"not a number"and exit cleanly with code 1. - Configure logging to write only to
app.logat DEBUG level (no console). Then issue onelog.debug, onelog.info, onelog.warning. Confirm all three appear inapp.log. - Stretch: modify the
day7_backup.pyexample so retries up to 2 extra times onConnectionError(with a shorttime.sleepbetween attempts) before giving up. Log each retry asWARNINGand the final failure asERROR.
Answers
Show answer 1
with open("hosts.txt") as f:
for line in f:
line = line.strip()
if line:
print(line)
Strip first, then test for truthiness — empty strings (and the trailing newline) are filtered out.
Show answer 2
from pathlib import Path
out = Path("out")
out.mkdir(exist_ok=True)
target = out / "result.txt"
target.write_text("test\n")
print(target.exists()) # True
One method per line — no string concatenation, works the same on Linux and Windows.
Show answer 3
import sys
try:
vlan = int(input("VLAN? "))
except ValueError:
print("not a number")
sys.exit(1)
print(f"got VLAN {vlan}")
Only catch the specific exception you can do something about. Letting other exceptions (Ctrl+C, etc.) propagate is a feature, not a bug.
Show answer 4
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.FileHandler("app.log")],
)
log = logging.getLogger("app")
log.debug("a debug message")
log.info("an info message")
log.warning("a warning")
By passing only a FileHandler in handlers=, console output is suppressed. Open app.log and all three lines should be present, level‑tagged.
Show answer 5
import time
def backup_with_retry(host, attempts=3, delay=1):
for n in range(1, attempts + 1):
try:
return fake_backup(host)
except ConnectionError as e:
if n == attempts:
log.error("%s: giving up after %d tries (%s)", host, attempts, e)
raise
log.warning("%s: attempt %d failed (%s) — retrying in %ds", host, n, e, delay)
time.sleep(delay)
Then in main, replace the inner try body with cfg = backup_with_retry(host) and catch ConnectionError the same way. The pattern — finite retries with backoff and clear logging on each — is the spine of every robust network‑facing script you’ll write.
That’s Week 1
You now have: Python installed, a venv per project, the syntax, the four core containers, control flow, functions and modules, files, exceptions, and logging. That is a complete enough toolbox to write real scripts — and it’s enough that the network‑specific libraries we hit next week will read like English instead of magic.
Coming next week
Day 8: The ipaddress Module — Subnets, Supernets, and Host Iteration. We start the network‑specific libraries with the one you’ll use weekly. By the end you’ll be summarizing prefixes, generating host lists, and answering “is this address inside that subnet?” in two lines.