You’ve got types, you’ve got strings, you’ve got containers. Today we wire them together with the small set of control‑flow keywords that make a script do something: if/elif/else for branches, for for iteration, while for waiting, plus the loop modifiers break, continue, and the helpers range, enumerate, and zip. By the end of today you’ll have a script that walks an inventory and produces a per‑site summary — the shape of every “do X to N devices” job you’ll ever write.
This is Day 5 of the 21‑post Python for Network Engineers series.
if / elif / else
def health(uptime_days, errors):
if errors > 100:
return "critical"
elif uptime_days > 365:
return "warning - reload overdue"
elif errors > 10:
return "warning - rising errors"
else:
return "ok"
print(health(uptime_days=400, errors=5)) # warning - reload overdue
print(health(uptime_days=30, errors=120)) # critical
The branches are tested top to bottom and only the first match runs. There’s no switch/case in older Python, but as of 3.10 there’s match/case for structural matching — we won’t use it in this series since plain if/elif is universally readable.
One Python convention worth absorbing now: comparisons can be chained. Instead of if mtu > 1500 and mtu < 9216: write if 1500 < mtu < 9216:. Reads exactly like the math.
for — the loop you’ll use 95% of the time
Python’s for iterates over any iterable: lists, tuples, dicts, sets, strings, file lines, generator output. There is no C‑style for (i=0; i<n; i++). If you want a counter, you ask for one explicitly:
devices = ["core-sw01", "edge-rtr02", "branch-sw01"]
for d in devices:
print(f"Connecting to {d}")
Need the index too? Use enumerate:
for idx, d in enumerate(devices, start=1):
print(f"[{idx}/{len(devices)}] {d}")
# [1/3] core-sw01
# [2/3] edge-rtr02
# [3/3] branch-sw01
Need to walk two equal‑length lists side by side? Use zip:
hosts = ["10.0.0.1", "10.0.0.2", "10.1.0.1"]
names = ["core-sw01", "edge-rtr02", "branch-sw01"]
for name, host in zip(names, hosts):
print(f"{name:<15} {host}")
Need a range of integers — say, VLANs 100 through 199? Use range:
for vlan in range(100, 200):
print(f"vlan {vlan}\n name USER_{vlan}")
range(start, stop, step) mirrors slicing — start inclusive, stop exclusive. range(10) alone gives 0..9.
break and continue — exiting and skipping
break exits the loop entirely. continue skips to the next iteration. Both refer to the innermost loop they’re inside.
for d in devices:
if d.startswith("core"):
continue # skip cores entirely
if d == "stop-here":
break # quit the whole loop
print(f"Processing {d}")
The classic network use of continue is filtering inside a loop where the alternative would be a deeply nested if. The classic use of break is “I found what I was looking for, I can stop scanning.”
while — for “keep checking until something happens”
import time
attempts = 0
max_attempts = 5
while attempts < max_attempts:
print(f"Pinging device... attempt {attempts + 1}")
# if some_check(): break
attempts += 1
time.sleep(2)
else:
# The else on a while runs if the loop finished without break.
print("Device never came up")
Use while when the number of iterations isn’t known up front — waiting for an interface to come up, polling an API for a job status, retrying a flapping SSH connection. The else clause on a loop runs only if the loop ended naturally (no break) — useful for “did we ever find it?” patterns.
Always have an exit condition. while True: with no break is an infinite loop, and you’ll only notice when your CPU fan kicks in.
Walking the inventory: a real script
Take the inventory dict from yesterday, group it by site, and produce a count per site:
inventory = {
"core-sw01": {"host": "10.0.0.1", "site": "hq", "device_type": "cisco_ios"},
"edge-rtr02": {"host": "10.0.0.2", "site": "hq", "device_type": "cisco_ios"},
"branch-sw01": {"host": "10.1.0.1", "site": "branch-a", "device_type": "cisco_nxos"},
"branch-rt01": {"host": "10.1.0.2", "site": "branch-a", "device_type": "cisco_ios"},
"lab-sw": {"host": "10.9.9.1", "site": "lab", "device_type": "arista_eos"},
}
by_site = {}
for name, attrs in inventory.items():
site = attrs["site"]
by_site.setdefault(site, []).append(name)
for site, devices in sorted(by_site.items()):
print(f"{site} ({len(devices)} devices):")
for d in devices:
print(f" - {d}")
Output:
branch-a (2 devices):
- branch-sw01
- branch-rt01
hq (2 devices):
- core-sw01
- edge-rtr02
lab (1 devices):
- lab-sw
Two control‑flow concepts worth pointing out: dict.setdefault(key, default) returns the existing value or initializes it on the first hit — a neat way to build “key → list” without an explicit if key not in d check. And sorted(some_dict.items()) gives you key‑sorted iteration without modifying the dict.
Skip the broken ones, count the rest
processed = 0
skipped = 0
for name, attrs in inventory.items():
if attrs["device_type"] not in {"cisco_ios", "cisco_nxos"}:
print(f" skipping {name} ({attrs['device_type']}) — not yet supported")
skipped += 1
continue
print(f" pretend SSH to {name} at {attrs['host']}")
processed += 1
print(f"\nDone — {processed} processed, {skipped} skipped")
This is the shape of nearly every multi‑device automation script: filter the inventory, do the work, count successes and failures, print a summary. Replace the print with ConnectHandler(**attrs) on Day 13 and it does real work.
Exercises
- Use
rangeand aforloop to print VLAN config for IDs 10, 20, 30, 40, 50 — each asvlan <id>on its own line. - Given
uptimes = {"sw1": 12, "sw2": 380, "sw3": 95, "sw4": 410}, print only the device names whose uptime is over 365 days. - Use
zipto print hostname/IP pairs from these two lists:names = ["a", "b", "c"]andips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]. Output one"<name> -> <ip>"per line. - Re‑use the
inventorydict from the worked example. Loop over it andbreakas soon as you find the first device whosesite == "lab". Print the device name when you find it. (If none found, print"no lab device"— hint: usefor ... else.) - Stretch: simulate a polling loop. Set
attempts = 0, then usewhile attempts < 10to “ping” a device. Generate a fake reachable status withrandom.choice([True, False]). Stop the loop and print the attempt number when reachable; otherwise print"never reachable"after 10 tries. Usetime.sleep(0.2)between attempts.
Answers
Show answer 1
for v in range(10, 51, 10):
print(f"vlan {v}")
# vlan 10
# vlan 20
# ...
# vlan 50
range(10, 51, 10) — start at 10, stop before 51, step by 10. The “stop is exclusive” rule is why we use 51, not 50.
Show answer 2
uptimes = {"sw1": 12, "sw2": 380, "sw3": 95, "sw4": 410}
for name, days in uptimes.items():
if days > 365:
print(name)
# sw2
# sw4
Show answer 3
names = ["a", "b", "c"]
ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
for name, ip in zip(names, ips):
print(f"{name} -> {ip}")
If the lists are unequal length, zip stops at the shorter one. Use itertools.zip_longest if you want to keep going with a fill value.
Show answer 4
for name, attrs in inventory.items():
if attrs["site"] == "lab":
print(f"found lab device: {name}")
break
else:
print("no lab device")
The else on a for only runs if the loop never executed break. It’s perfect for “did the search find anything?” without a separate flag variable.
Show answer 5
import random, time
attempts = 0
while attempts < 10:
attempts += 1
reachable = random.choice([True, False])
print(f"Attempt {attempts}: reachable={reachable}")
if reachable:
print(f"Device came up on attempt {attempts}")
break
time.sleep(0.2)
else:
print("never reachable")
Same for/while ... else trick — the else only fires if the loop finished without a break, which here means we exhausted all 10 attempts.
Coming tomorrow
Day 6: Functions and Modules — Building Reusable Network Helpers. We’ll take the patterns we’ve been writing inline and pull them into named functions, then split them across files. By the end of tomorrow you’ll have a tiny net_utils.py module with three reusable helpers and a script that imports from it.