Every meaningful network script revolves around a small data structure: a list of devices, a dict of interface counters, a set of allowed VLANs. Today we cover the four built‑in containers Python ships with — list, tuple, dict, set — and the right one to reach for in each network situation. By the end you’ll have a working device inventory in pure Python, ready to loop over starting tomorrow.
This is Day 4 of the 21‑post Python for Network Engineers series.
The four containers, in one table
| Type | Looks like | Ordered? | Mutable? | Duplicates? | Use it for |
|---|---|---|---|---|---|
list |
[1, 2, 3] |
Yes | Yes | Yes | A sequence you’ll add to / iterate over |
tuple |
(1, 2, 3) |
Yes | No | Yes | A fixed record (e.g. an IP/port pair) |
dict |
{"k": "v"} |
Yes (3.7+) | Yes | Keys unique | Lookup by name (device by hostname) |
set |
{1, 2, 3} |
No | Yes | No | Membership tests, deduping, set algebra |
Spend ten seconds memorizing this table — it’ll save you many wrong choices.
Lists — the workhorse
vlans = [10, 20, 30, 99]
vlans.append(100) # add to end
vlans.insert(0, 1) # insert at index 0
vlans.remove(99) # remove first occurrence by value
last = vlans.pop() # remove and return last
print(len(vlans), vlans)
# 5 [1, 10, 20, 30]
Lists are indexable and sliceable just like strings: vlans[0], vlans[-1], vlans[1:3]. They can hold mixed types, but in practice keep elements homogeneous — a list of ints, a list of dicts, a list of devices. Mixed‑type lists are usually a sign the data wants to be a dict instead.
The list comprehension is the idiom that separates beginner Python from intermediate Python. Two equivalent ways to build “interfaces with VLAN > 100”:
# Verbose:
high_vlans = []
for v in vlans:
if v > 100:
high_vlans.append(v)
# Idiomatic:
high_vlans = [v for v in vlans if v > 100]
Once you can read the second form fluently, half the Python you’ll see in the wild becomes legible. The shape is always [expression for item in iterable if condition], where the if is optional.
Tuples — the immutable record
target = ("10.0.0.1", 22) # IP and port — order matters, never changes
ip, port = target # unpack into two variables
print(ip, port)
# 10.0.0.1 22
Tuples have one job: bundle a fixed number of related values that won’t change. The classic network use is an (ip, port) pair for sockets. Functions that return “two related things” — like a Netmiko prompt and the rest of the output — return a tuple, and you unpack it.
Because tuples are immutable, they’re hashable and can be used as dict keys or set members. Lists can’t. We’ll use that property when we build (vrf, prefix) → next‑hop lookups later in the series.
Dicts — the heart of every Netmiko/NAPALM call
If you only learn one container deeply, make it the dict. Every modern network library uses dicts to represent devices, configurations, and parsed output.
device = {
"device_type": "cisco_ios",
"host": "10.0.0.1",
"username": "admin",
"password": "C1sco12345",
"secret": "enable_pw",
}
print(device["host"]) # 10.0.0.1 — KeyError if missing
print(device.get("port", 22)) # 22 — never raises, returns default
device["port"] = 2222 # add or update a key
del device["secret"] # remove
print("host" in device) # True — membership is by key, not value
print(list(device.keys()))
print(list(device.values()))
print(list(device.items())) # list of (key, value) tuples
Cisco context: when you call ConnectHandler(**device) in Netmiko, the ** spreads the dict into keyword arguments. So the dict above becomes ConnectHandler(device_type="cisco_ios", host="10.0.0.1", ...) — the keys must match the parameter names exactly. This is also why YAML inventories (Day 15) deserialize cleanly into Python dicts and feed straight into the connection libraries.
Iterating dicts the right way
counters = {"Gi0/0": 12345, "Gi0/1": 67890, "Gi0/2": 0}
for name, pkts in counters.items():
print(f"{name}: {pkts:,} packets")
# Gi0/0: 12,345 packets
# Gi0/1: 67,890 packets
# Gi0/2: 0 packets
dict.items() gives you the (key, value) pairs in one go. Avoid the beginner pattern of looping over keys and indexing the dict on each iteration (for k in d: d[k]) — it’s two lookups instead of one and reads worse.
Nested dicts — the natural shape of an inventory
inventory = {
"core-sw01": {
"host": "10.0.0.1",
"device_type": "cisco_ios",
"site": "hq",
},
"edge-rtr02": {
"host": "10.0.0.2",
"device_type": "cisco_ios",
"site": "hq",
},
"branch-sw01": {
"host": "10.1.0.1",
"device_type": "cisco_nxos",
"site": "branch-a",
},
}
print(inventory["core-sw01"]["host"]) # 10.0.0.1
This shape — outer dict keyed by hostname, inner dict of attributes — is exactly what Nornir, Ansible, and most home‑grown inventories use. Today it’s hardcoded; on Day 15 we’ll load it from a YAML file with one line.
Sets — for “is this in the allowed VLAN list?”
allowed_vlans = {10, 20, 30, 100, 200}
trunked_vlans = {10, 20, 50, 200}
print(50 in allowed_vlans) # False
print(allowed_vlans & trunked_vlans) # intersection: {10, 20, 200}
print(trunked_vlans - allowed_vlans) # difference: {50} — VLANs trunked but not allowed
print(allowed_vlans | trunked_vlans) # union
Sets dedupe automatically (set([1, 1, 2, 3]) == {1, 2, 3}) and give you O(1) membership tests — checking x in some_set doesn’t slow down with size, while x in some_list does. For “list of allowed VLANs” or “set of devices we’ve already polled,” reach for a set first.
The & | - operators map to set algebra and are the cleanest way to answer questions like “which VLANs are trunked but not allowed” — that one line of code is a complete config‑drift check.
Putting it together: filter an inventory by site
inventory = {
"core-sw01": {"host": "10.0.0.1", "site": "hq"},
"edge-rtr02": {"host": "10.0.0.2", "site": "hq"},
"branch-sw01": {"host": "10.1.0.1", "site": "branch-a"},
"branch-rt01": {"host": "10.1.0.2", "site": "branch-a"},
}
hq_hosts = [d["host"] for d in inventory.values() if d["site"] == "hq"]
print(hq_hosts)
# ['10.0.0.1', '10.0.0.2']
One list comprehension, no helper function, no temporary variables. This is the shape of nearly every “do X to a subset of devices” loop you’ll write in this series.
Exercises
- Create a list of five interface names. Print just the third one (use indexing) and the last two (use a slice).
- You have
vlans = [10, 20, 20, 30, 30, 30, 40]. Use a set to print just the unique VLAN IDs (no duplicates). Compare the length of the original list and the set. - Build a dict for a single device with at least four keys (
host,device_type,username,password). Then print just itshostsafely — so that if the key is missing it returns the string"unknown"instead of raising. - Given the
inventorydict from the worked example above, write one comprehension that returns a list of hostnames whosedevice_typeis"cisco_ios". Hint: iterate overinventory.items(). - Stretch: you have two sets —
configured = {"Gi0/0", "Gi0/1", "Gi0/2"}andoperational = {"Gi0/0", "Gi0/2", "Gi0/3"}. Print three lines: which interfaces are both configured and operational, which are configured but not operational (probably down), and which are operational but not configured (someone added a port without telling you).
Answers
Show answer 1
intfs = ["Gi0/0", "Gi0/1", "Gi0/2", "Gi0/3", "Gi0/4"]
print(intfs[2]) # Gi0/2
print(intfs[-2:]) # ['Gi0/3', 'Gi0/4']
Show answer 2
vlans = [10, 20, 20, 30, 30, 30, 40]
unique = set(vlans)
print(unique) # {10, 20, 30, 40} (order not guaranteed)
print(len(vlans), len(unique)) # 7 4
Wrapping a list in set() is the canonical Python way to dedupe. If you need to preserve order, list(dict.fromkeys(vlans)) works in 3.7+.
Show answer 3
device = {
"host": "10.0.0.1",
"device_type": "cisco_ios",
"username": "admin",
"password": "C1sco12345",
}
print(device.get("host", "unknown")) # 10.0.0.1
print(device.get("vrf", "unknown")) # unknown
dict.get(key, default) returns the default instead of raising KeyError — use it whenever the key might legitimately be absent.
Show answer 4
ios_devices = [name for name, attrs in inventory.items()
if attrs["device_type"] == "cisco_ios"]
print(ios_devices)
# ['core-sw01', 'edge-rtr02', 'branch-rt01'] (depending on inventory contents)
Unpacking the (key, value) pair right in the for clause keeps the comprehension readable. If you need to also check whether the key exists first, use attrs.get("device_type") == "cisco_ios".
Show answer 5
configured = {"Gi0/0", "Gi0/1", "Gi0/2"}
operational = {"Gi0/0", "Gi0/2", "Gi0/3"}
print("OK (both): ", configured & operational) # {'Gi0/0', 'Gi0/2'}
print("Down (cfg only): ", configured - operational) # {'Gi0/1'}
print("Rogue (op only): ", operational - configured) # {'Gi0/3'}
Three lines of set algebra produce a complete configuration‑vs‑reality drift report. This idea — compare what you intended with what’s actually running — is the core of every config compliance tool you’ll ever write.
Coming tomorrow
Day 5: Control Flow — Looping Through Interfaces and Devices. We’ll wire up for, while, if/elif/else, break, and continue against the inventory we built today. By the end of tomorrow you’ll have a script that walks every device in the inventory and prints a per‑site summary.