Dictionaries: data with names
Three lessons in, you could track an inventory as parallel lists — hostnames in one, IPs in another, models in a third — and pray the indexes stay aligned. Don’t. A dictionary keeps each fact attached to its name:
sw1 = {
"hostname": "den-acc-sw01",
"ip": "10.20.30.11",
"model": "C9200L-48P-4X",
}
Read values by key, write the same way:
>>> sw1["ip"]
'10.20.30.11'
>>> sw1["serial"] = "FOC2217A0AB" # add a new key
>>> "model" in sw1 # membership checks the KEYS
True
Ask for a key that isn’t there with square brackets and you get a
KeyError — a crash. The professional habit is .get(), which makes a
missing key a decision instead:
>>> sw1.get("uptime", "unknown")
'unknown'
That one method is the difference between an inventory report with a few “unknown” cells and a script that dies at device 247 of 300 because one switch never reported uptime.
Real inventories nest
One device is a dict; a fleet is a dict of dicts, keyed by hostname:
inventory = {
"den-acc-sw01": {"ip": "10.20.30.11", "model": "C9200L-48P-4X"},
"den-acc-sw02": {"ip": "10.20.30.12", "model": "C9200L-48P-4X"},
}
>>> inventory["den-acc-sw01"]["ip"] # read inside-out, left to right
'10.20.30.11'
Loop a dict with .items(), which hands you key and value together:
for hostname, facts in inventory.items():
print(f"{hostname:<16} {facts['ip']:<14} {facts['model']}")
Get comfortable with this shape now — every network API you will ever
call returns nested dictionaries. When you hit REST, NETCONF, and
structured show output in the flagship course, the payloads are this,
three levels deeper.
Sets: the drift detector
A set is an unordered collection of unique values. Deduplication is the warm-up trick:
>>> seen = [10, 20, 10, 30, 20]
>>> set(seen)
{10, 20, 30}
The real power is the operators. Subtract one set from another and you get what’s in the first but not the second:
>>> intended = {10, 20, 30, 40} # what the design says
>>> actual = {10, 20, 50} # what the switch says
>>> intended - actual # missing from the switch
{30, 40}
>>> actual - intended # rogue — on the switch, not the design
{50}
>>> intended & actual # in both
{10, 20}
That’s config drift detection — the entire concept — in three lines.
Exceptions: one bad input shouldn’t kill the run
You’ve already met exceptions — KeyError, ValueError, the
AttributeError from Lesson 1’s .strip typo. So far they’ve meant a dead
script. try/except turns them into handled cases:
raw = "twenty" # parsed from somewhere ugly
try:
vlan = int(raw)
except ValueError:
print(f"skipping unparseable VLAN: {raw!r}")
vlan = None
The block under try is the risky part; the block under except runs only
if that specific exception fires. At fleet scale this is survival: when
device 113’s output is malformed, you want one logged skip — not three
hours of work lost at 2 AM.
Catch the specific exception you expect. The lazy form:
try:
vlan = int(raw)
except: # ← catches EVERYTHING, including your own typos
vlan = None
For dict lookups you now have two idioms — .get() when a default is fine,
try/except KeyError when a missing key needs real handling (logging,
counting, alerting). Both beat the crash.
Output appears here. First run downloads the Python runtime (~10 MB), so give it a few seconds.
Exercises (graded)
cd labs/python-foundations/lesson04
pytest -q
Five functions in exercises.py:
device_facts(hostname, ip, model)— build the standard device dictsafe_get_ip(inventory, hostname)— nested lookup that returnsNoneinstead of crashingunique_vlans(vlan_lists)— collapse many VLAN lists into one sorted, deduped listmissing_vlans(intended, actual)— the drift report: sorted list of what’s missingparse_vlan_id(text)—int()with a safety net:Nonefor garbage input
Your report script crashes with KeyError: "serial" halfway through the fleet. Which change makes it finish, marking unknowns?
intended = {10, 20, 30, 40} and actual = {10, 20, 50}. What is intended - actual?
Why is "except ValueError:" better than a bare "except:" around int(raw)?
Summary
Dictionaries attach names to facts — sw1["ip"], .get() for the keys
that might not be there, .items() for looping, and nesting for fleets,
which is the exact shape every API response takes. Sets answer membership
questions at a glance: dedupe with set(), and read config drift straight
off the subtraction operator (intended - actual). Exceptions make
failure a handled case — catch the specific error you expect
(ValueError, KeyError) and let everything else crash loudly, because a
bare except: converts your own bugs into silent wrong answers. Next
lesson: regular expressions — pulling structured facts out of show output
that .split() can’t reach.