Dictionaries, Sets, and Exceptions

Parallel lists stop scaling the moment a device has more than one fact. Dictionaries give your data names, sets answer "what's different between these two switches" in one operator, and exceptions keep one bad input from killing a 300-device run.

TL;DR - only got 10 minutes? This is the lesson. Hit to have it read to you.
  • A dict maps keys to values: sw1 = {"hostname": "acc-sw01", "ip": "10.20.30.11"}. Read with sw1["ip"]; a missing key raises KeyError.
  • sw1.get("serial", "unknown") returns a default instead of crashing - the difference between a report that says "unknown" and a script that dies at device 247.
  • Loop a dict with for hostname, facts in inventory.items() - and expect nesting: every API you will ever call returns dicts inside dicts.
  • A set holds unique values. set(vlan_list) dedupes; intended - actual is your config-drift report in one operator.
  • Empty braces {} make a DICT, not a set - use set() for an empty set.
  • try/except turns crashes into decisions: catch ValueError around int(text), KeyError around lookups. Catch SPECIFIC exceptions - a bare except: hides your own typos.
  • Prove it: the graded lab at the end runs in your browser - implement the functions and make every check green.
In this lesson you will:
  • Model device inventories as dictionaries instead of parallel lists
  • Read nested dict structures - the shape every network API returns
  • Use .get() to make missing keys a decision instead of a crash
  • Detect config drift with set difference in one line
  • Catch specific exceptions so bad input skips instead of kills

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": "acc-sw01",
    "ip": "10.20.30.11",
    "model": "C9300-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 = {
    "acc-sw01": {"ip": "10.20.30.11", "model": "C9300-48P-4X"},
    "acc-sw02": {"ip": "10.20.30.12", "model": "C9300-48P-4X"},
}

>>> inventory["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.

Drift detection is two subtractions
Rendering diagram…
View diagram source - it's just text (Mermaid). Diagrams-as-code is how modern network docs work; the flagship course has a free module on it.
flowchart TD
  I["intended<br/>{10, 20, 30, 40}"] --> M["intended - actual"]
  A["actual<br/>{10, 20, 50}"] --> M
  I --> R["actual - intended"]
  A --> R
  M --> MO["missing from switch:<br/>{30, 40}"]
  R --> RO["rogue on switch:<br/>{50}"]

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.

Inventories, drift detection, and graceful failure
Try it yourself (Python runs in your browser)
Output appears here. First run downloads the Python runtime (~10 MB), so give it a few seconds.

Prove it (graded lab)

Write each function in the editor, then Run grader - it runs the lesson’s real checks right here in your browser, nothing to install. Green means the skill is yours, and these are the building blocks the Lesson 10 capstone (netaudit) is assembled from.

Five functions:

  1. device_facts(hostname, ip, model) - build the standard device dict
  2. safe_get_ip(inventory, hostname) - nested lookup that returns None instead of crashing
  3. unique_vlans(vlan_lists) - collapse many VLAN lists into one sorted, deduped list
  4. missing_vlans(intended, actual) - the drift report: sorted list of what’s missing
  5. parse_vlan_id(text) - int() with a safety net: None for garbage input
Graded lab - 5 checks, run right here
Edit the functions above, then Run grader. First run downloads the Python runtime (~10 MB), so give it a few seconds.
Reveal a reference solution
"""Lesson 4 reference solutions. Read after an honest attempt."""


def device_facts(hostname, ip, model):
    return {"hostname": hostname, "ip": ip, "model": model}


def safe_get_ip(inventory, hostname):
    return inventory.get(hostname, {}).get("ip")


def unique_vlans(vlan_lists):
    found = set()
    for vlans in vlan_lists:
        for vlan in vlans:
            found.add(vlan)
    return sorted(found)


def missing_vlans(intended, actual):
    return sorted(set(intended) - set(actual))


def parse_vlan_id(text):
    try:
        return int(text.strip())
    except ValueError:
        return None

Read it, close it, then re-type your own version from memory. Wrong turns are where the learning is.

Check your understanding

Your report script crashes with KeyError: "serial" halfway through the fleet. Which change makes it finish, marking unknowns?

1 / 3

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.