Control Flow: Looping Through Interfaces and Devices

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

  1. Use range and a for loop to print VLAN config for IDs 10, 20, 30, 40, 50 — each as vlan <id> on its own line.
  2. Given uptimes = {"sw1": 12, "sw2": 380, "sw3": 95, "sw4": 410}, print only the device names whose uptime is over 365 days.
  3. Use zip to print hostname/IP pairs from these two lists: names = ["a", "b", "c"] and ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3"]. Output one "<name> -> <ip>" per line.
  4. Re‑use the inventory dict from the worked example. Loop over it and break as soon as you find the first device whose site == "lab". Print the device name when you find it. (If none found, print "no lab device" — hint: use for ... else.)
  5. Stretch: simulate a polling loop. Set attempts = 0, then use while attempts < 10 to “ping” a device. Generate a fake reachable status with random.choice([True, False]). Stop the loop and print the attempt number when reachable; otherwise print "never reachable" after 10 tries. Use time.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.

Leave a Reply