Python for Network Engineers ยท Python from zero

Python 2: lists, dictionaries, loops & decisions

Store many devices in a list, describe each one with a dictionary, repeat work with for loops and let the script decide with if/elif/else. Then loop over three live routers and flag the one with a down interface.

33 min read9 chapters2 labs15 quiz7 scenarios15 interview Q&A

This first module is free: read the lesson and take the quiz. Create a free account to run up to 3 hands-on labs.

Log inStart free
Jump to chapter (9)
01

Python 2: what you will learn and the big picture

What you will learn. In "Python 1" you stored one value in one variable. Real networks have many devices, so now you will store many values and let the script repeat work and decide. You will learn lists (many values in order), dictionaries (a value looked up by name), for loops (do it for every item), and if / elif / else (do it only when a condition is true). In the labs you build a small device inventory and then loop over three live routers, count their up interfaces and flag one that has a down interface.

Prerequisites. The module "Python 1": variables, strings, f-strings, comparisons and reading an error message. If the word f-string worries you, revisit that module first.

Analogy: a spreadsheet

Think of the inventory spreadsheet your team already keeps. A list is one column: every router IP, in order. A dictionary is one row with headers: name, IP, role, site and the values next to them. A for loop is "go down the rows, one by one". An if is a conditional rule: "if role is core, colour the row red". With only those four ideas you can read almost any structured data a network script meets.

Lista column of IPsDictionarya row: key to valuefor loopevery row in turnif / elsechoose what to do

The four ideas of this module: store, label, repeat, decide.

What the finished labs produce

Lab 1 turns a dictionary of four devices (plus one you add) into three small reports. This is the real output of the finished inventory.py:

AUTO> python3 inventory.py
All devices:
  PUN-CORE-01 -> 10.10.0.1
  PUN-ACC-01 -> 10.10.0.11
  BLR-CORE-01 -> 10.20.0.1
  BLR-ACC-01 -> 10.20.0.11
  HYD-ACC-01 -> 10.30.0.11
Total: 5
Core devices:
  PUN-CORE-01
  BLR-CORE-01
Per site: {'Pune': 2, 'Bengaluru': 2, 'Hyderabad': 1}

Lab 2 logs in to three routers, reads the interface table of each, counts the up interfaces and warns about any interface that is down (but not about a port that was shut on purpose):

AUTO> python3 check_up.py
MUM-R1: 2 interfaces up
MUM-R2: 3 interfaces up
MUM-R3: 2 interfaces up
  WARNING MUM-R3: down -> ['GigabitEthernet0/2']
Check finished

By the end of the module you will have written the lines that make this output appear.

The nine chapters

  1. Big picture (this chapter)
  2. Lists
  3. Dictionaries
  4. Nested data: an inventory
  5. for loops and range()
  6. Decisions: if / elif / else
  7. Counting and collecting (Lab 1)
  8. Loop over routers (Lab 2)
  9. Summary and checklist

Common mistake. Skipping straight to the labs. The labs ask you to edit specific line numbers with exact indentation; a quick read of chapters 2 to 6 saves a lot of frustration.

Exam trap. The 350-901 exam is full of lists and dictionaries, because API answers (JSON) are exactly nested lists and dictionaries. If you can read a loop over a dictionary, you can read most API-handling code on the exam.

The spreadsheet that became code

A team kept 300 devices in a spreadsheet and copied rows into scripts by hand. They moved the same columns into a Python dictionary, one entry per device. Now one loop can answer "which core devices are in Pune?" and the data is checked into version control instead of living on one laptop.

Lesson: data in lists and dictionaries is something a program can search and count.

"Why are lists and dictionaries so important for network automation?"

Device inventories, parsed show output and API responses (JSON) are all made of nested lists and dictionaries. Lists hold ordered collections such as device IPs; dictionaries hold labelled fields such as an interface's IP and status. Loops and conditions then process them.

Key takeaways

  • A list is many values in order; a dictionary looks a value up by its key.
  • A for loop repeats work for every item; if / elif / else chooses what to do.
  • Labs: build an inventory, then loop over three routers and flag a down interface.
  • These shapes are exactly what API data (JSON) looks like.
02

Lists: many values in one variable

So far one variable held one value. But you manage many routers, many VLANs, many interfaces. A list is a single variable that holds several values in order. You write it with square brackets, and separate the items with commas. Think of the list of router IPs on a whiteboard, one under another.

routers = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
vlans = [10, 20, 30]
print(routers)
print(vlans)
print(type(routers))
print(len(routers))
AUTO> python3 l1.py
['10.99.0.1', '10.99.0.2', '10.99.0.3']
[10, 20, 30]
<class 'list'>
3

len() counts the items. A list can hold any type, strings and numbers alike, and may be empty: down = [] is a list with nothing in it yet.

Index: the position number

Each item has an index, its position. Python counts from 0: the first item is index 0, the second is 1. A negative index counts from the end: -1 is the last item. A slice with a colon takes a part: [1:3] means from index 1 up to, but not including, 3.

"10.99.0.1" "10.99.0.2" "10.99.0.3" "10.99.0.4" [0] [-4] [1] [-3] [2] [-2] [3] [-1]

Each item has a positive index counting from 0 and a negative index counting from the end.

routers = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
print(routers[0])
print(routers[2])
print(routers[-1])
print(routers[1:3])
print(len(routers))
AUTO> python3 l2.py
10.99.0.1
10.99.0.3
10.99.0.3
['10.99.0.2', '10.99.0.3']
3

Changing a list

Lists can grow and shrink. append() adds one item at the end, remove() deletes the first matching item, and in asks whether a value is present. sorted() gives a sorted copy.

routers = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
routers.append("10.99.0.4")
print(routers)
print("10.99.0.9" in routers)
routers.remove("10.99.0.2")
print(routers)
print(len(routers))
print(sorted([30, 10, 20]))
AUTO> python3 l3.py
['10.99.0.1', '10.99.0.2', '10.99.0.3', '10.99.0.4']
False
['10.99.0.1', '10.99.0.3', '10.99.0.4']
3
[10, 20, 30]

Notice that append and remove change the list itself (unlike string methods, which return a new string). Lists are changeable; strings are not.

The index that does not exist

Asking for a position beyond the end is an IndexError. The counting from 0 trips up everyone once:

routers = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
print(routers[3])
AUTO> python3 l4.py
Traceback (most recent call last):
  File "l4.py", line 2, in <module>
    print(routers[3])
IndexError: list index out of range

The list has 3 items, so the valid indexes are 0, 1 and 2. A list of length n has valid indexes 0 to n - 1.

Worked example. Your VLAN list is vlans = [10, 20, 30]. vlans[0] is 10, vlans[-1] is 30, 20 in vlans is True. After vlans.append(40), len(vlans) is 4.

Common mistake. Using index 1 for the first item (Python starts at 0), and using len(routers) itself as an index. The last valid index is len(routers) - 1, or simply -1.

Exam trap. Typical question: for x = ["a", "b", "c"] what are x[1], x[-1] and x[0:2]? Answers: "b", "c", ["a", "b"]. Also know that append modifies the list in place and returns nothing useful.

Counting from one

A script checked routers[1] expecting the first router and silently skipped router one for weeks. Nothing crashed, so nobody noticed until an audit showed one router had never been checked. The fix was a single character, routers[0], and a loop so that no index was typed by hand again.

Lesson: a wrong index may not crash; loops remove the need to pick indexes by hand.

"How do indexes work in a Python list?"

Indexes start at 0, so x[0] is the first item and x[-1] the last. A slice x[a:b] includes a but excludes b. Going past the end raises an IndexError. A list of length n has valid indexes 0 to n - 1.

Key takeaways

  • A list is written in square brackets; items are separated by commas.
  • Indexes start at 0; -1 is the last item; a slice excludes its end.
  • append adds, remove deletes, in tests, len counts.
  • A missing position is an IndexError.
03

Dictionaries: key to value, like an ARP table

A list finds things by position. But "the third column" is a poor way to ask for an IP address. You would rather say "the IP of this device". A dictionary (dict) finds a value by a key, a label you choose. You already know one: the ARP table. You look up an IP address (the key) and get a MAC address (the value). A dictionary is written with curly brackets, and a colon joins each key to its value.

key value "ip" "10.1.1.1" Find by the key, not by position dev["ip"] gives "10.1.1.1"

A dictionary is a set of key and value pairs.

dev = {"ip": "10.1.1.1", "role": "core"}
print(dev)
print(dev["ip"])
print(dev["role"])
print(len(dev))
AUTO> python3 d1.py
{'ip': '10.1.1.1', 'role': 'core'}
10.1.1.1
core
2

Adding, changing and asking

  • dev["site"] = "Pune" adds a new pair, or replaces the value if the key already exists.
  • dev.get("os", "unknown") reads a key safely: if the key is missing it returns the default you gave instead of an error.
  • "ip" in dev asks whether a key is present.
  • keys() and values() list the keys and the values.
dev = {"ip": "10.1.1.1", "role": "core"}
dev["site"] = "Pune"
dev["role"] = "edge"
print(dev)
print(dev.get("os", "unknown"))
print("ip" in dev)
print("os" in dev)
print(list(dev.keys()))
print(list(dev.values()))
AUTO> python3 d2.py
{'ip': '10.1.1.1', 'role': 'edge', 'site': 'Pune'}
unknown
True
False
['ip', 'role', 'site']
['10.1.1.1', 'edge', 'Pune']

The key that does not exist

Reading a missing key with square brackets is a KeyError. Keys are case-sensitive, and they are text, so "IP" is not "ip":

dev = {"ip": "10.1.1.1", "role": "core"}
print(dev["IP"])
AUTO> python3 d3.py
Traceback (most recent call last):
  File "d3.py", line 2, in <module>
    print(dev["IP"])
KeyError: 'IP'

The error message shows the missing key itself: KeyError: 'IP'. When you are not sure a key exists, use get() or test with in first.

List or dictionary?

You need...Use
An ordered group of similar things (all router IPs)A list
Labelled fields of one thing (IP, role, site of a device)A dictionary
Look something up by name, quicklyA dictionary
Do something for every itemEither, with a for loop

Worked example. The interface table of a router is naturally a dictionary: the key is the interface name and the value says up or down. status["GigabitEthernet0/1"] answers "is Gi0/1 up?" without counting positions.

Common mistake. Mixing up the brackets: lists use [ ], dictionaries use { }, but reading a value from either uses [ ]: dev["ip"]. And do not forget the quotes around a text key: dev[ip] without quotes looks for a variable named ip.

Exam trap. d["x"] on a missing key raises KeyError; d.get("x") returns None (or your default). in on a dictionary tests the keys, not the values. Dictionary keys must be unique: assigning the same key again replaces the value.

The missing key at 2 a.m.

A monitoring script read device["os"] for every device. One new device in the inventory had no "os" field and the script crashed with KeyError: 'os' during the night. The fix was device.get("os", "unknown"), so a missing field no longer stopped the report.

Lesson: use get() with a default when data may be incomplete.

"When would you use a dictionary instead of a list?"

When values have names and you want to look them up by name, for example the fields of one device or interface statuses by interface name. Dictionary lookup by key is direct and readable. Use a list when you have an ordered collection of similar items to loop over.

Key takeaways

  • A dictionary maps keys to values with curly brackets and colons.
  • Read with d["key"]; add or change with d["key"] = value; read safely with get().
  • in tests keys; keys are case-sensitive and unique.
  • A missing key is a KeyError.
04

Nested data: an inventory in a dictionary

A real inventory has many devices and each device has several facts. You get that by putting one container inside another, called nesting. Think of a filing cabinet: the drawer label is the key, and inside the drawer is a folder with its own labelled pages. The shape dictionary of dictionaries is how most inventories look, and it is also the shape of JSON that APIs return, so learning it now pays off in every later module.

inventory "PUN-CORE-01" "PUN-ACC-01" {"ip": "10.10.0.1", "role": "core", "site": "Pune"} {"ip": "10.10.0.11", "role": "access", "site": "Pune"}

The outer dictionary is keyed by device name; each value is a small dictionary of facts.

This is the first part of the inventory in Lab 1. Read the shape from the outside in. The outer curly brackets open the inventory. Each line is "device name": { ...facts... }. Here it is in action:

inventory = {
    "PUN-CORE-01": {"ip": "10.10.0.1", "role": "core", "site": "Pune"},
    "PUN-ACC-01": {"ip": "10.10.0.11", "role": "access", "site": "Pune"},
    "BLR-CORE-01": {"ip": "10.20.0.1", "role": "core", "site": "Bengaluru"},
    "BLR-ACC-01": {"ip": "10.20.0.11", "role": "access", "site": "Bengaluru"},
}
print(inventory["PUN-CORE-01"])
print(inventory["PUN-CORE-01"]["ip"])
print(len(inventory))
inventory["HYD-ACC-01"] = {"ip": "10.30.0.11", "role": "access", "site": "Hyderabad"}
print(len(inventory))
print(inventory["HYD-ACC-01"]["site"])
print("BLR-CORE-01" in inventory)
AUTO> python3 n1.py
{'ip': '10.10.0.1', 'role': 'core', 'site': 'Pune'}
10.10.0.1
4
5
Hyderabad
True
  • inventory["PUN-CORE-01"] returns the inner dictionary for that device.
  • inventory["PUN-CORE-01"]["ip"] chains two lookups: first the device, then its ip field.
  • len(inventory) counts devices (outer keys), not facts.
  • Adding a device is one new line: assign a new outer key to a new small dictionary. The count went from 4 to 5.

Another shape: a list of dictionaries

When the order matters or names can repeat, people use a list of dictionaries: each device is a dictionary and the list holds them. Reading uses an index first, then a key. JSON from APIs often has this shape.

devices = [
    {"name": "R1", "ip": "10.99.0.1", "role": "core"},
    {"name": "R2", "ip": "10.99.0.2", "role": "edge"},
]
print(devices[0])
print(devices[1]["ip"])
print(len(devices))
devices.append({"name": "R3", "ip": "10.99.0.3", "role": "edge"})
print(devices[-1]["name"])
AUTO> python3 n2.py
{'name': 'R1', 'ip': '10.99.0.1', 'role': 'core'}
10.99.0.2
2
R3

Both shapes work. The key skill is to read the brackets: a number or a name inside [ ] tells you whether the thing in front of it is a list or a dictionary.

Missing facts

Nested data makes missing-key errors more likely, because a key may be absent in one device and present in another. The error names the missing key:

inventory = {"R1": {"ip": "10.99.0.1"}}
print(inventory["R1"]["os"])
AUTO> python3 n3.py
Traceback (most recent call last):
  File "n3.py", line 2, in <module>
    print(inventory["R1"]["os"])
KeyError: 'os'

Worked example. Lab 1 asks you to add HYD-ACC-01 to the inventory. It is one line inside the outer braces: the key "HYD-ACC-01", a colon, then a small dictionary with ip, role and site, and a comma at the end because more items may follow.

Common mistake. Forgetting a comma between two entries, or an extra opening bracket without its closing partner. Python reports a SyntaxError and points near the problem. Count your opening and closing brackets for each level.

Exam trap. Be able to evaluate chained lookups by hand: for d = {"R1": {"ip": "10.1.1.1"}}, d["R1"]["ip"] is "10.1.1.1". For a list of dictionaries, devices[0]["name"] reads index first, then key.

Inventory as the single source

A team kept the device list in three scripts, each slightly different. After they moved it into one dictionary in one file and imported it everywhere, adding a router meant editing one entry. The audit finding "inconsistent device lists" disappeared.

Lesson: keep data in one structured place and let every script read it.

"What does data[0]["ip"] tell you about the shape of data?"

It is a list (index 0) whose items are dictionaries (key "ip"). Reading the brackets from left to right tells you the structure: a number picks an item of a list, a key picks a value of a dictionary.

Key takeaways

  • Nesting puts a container inside another: a dictionary of dictionaries is an inventory.
  • Chain lookups: inventory["R1"]["ip"].
  • A list of dictionaries is the other common shape (JSON from APIs).
  • Read brackets to know what is a list and what is a dictionary.
05

for loops and range(): do it for every item

On a switch you type interface range gigabitEthernet 0/1 - 4 and the next commands apply to four ports. You do not repeat them four times. A for loop is that idea in Python: "for every item in this group, run these lines". It is the single most important tool in automation, because it turns a script for one router into a script for a thousand.

Anatomy of a for loop

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]
for ip in ROUTERS:
    print("Connecting to", ip)
print("All done")
AUTO> python3 o1.py
Connecting to 10.99.0.1
Connecting to 10.99.0.2
Connecting to 10.99.0.3
All done
  1. for ip in ROUTERS: reads "for each item in ROUTERS, call it ip". The variable ip is created by the loop and takes each value in turn.
  2. The line ends with a colon.
  3. The lines indented by 4 spaces form the body. They run once per item.
  4. The first line that is not indented (print("All done")) is outside the loop and runs once, after the loop ends.
next item run the body more items? yes: repeat no: done

The loop picks the next item, runs the body, and asks whether more items remain.

Looping over numbers with range()

range(1, 4) produces 1, 2, 3: it starts at the first number and stops before the second. range(3) gives 0, 1, 2. It is perfect for repeated configuration lines.

for n in range(1, 4):
    print(f"interface Loopback{n}")
for n in range(3):
    print(n)
AUTO> python3 o2.py
interface Loopback1
interface Loopback2
interface Loopback3
0
1
2

Looping over a dictionary

A plain loop over a dictionary gives the keys. The method items() gives the key and the value together, so you can unpack them into two variables:

inventory = {
    "PUN-CORE-01": {"ip": "10.10.0.1", "role": "core"},
    "PUN-ACC-01": {"ip": "10.10.0.11", "role": "access"},
}
for name, info in inventory.items():
    print(name, info["ip"])
for name in inventory:
    print(name)
AUTO> python3 o3.py
PUN-CORE-01 10.10.0.1
PUN-ACC-01 10.10.0.11
PUN-CORE-01
PUN-ACC-01

Counters and enumerate

A counter starts at 0 and adds 1 inside the loop. enumerate() hands you a running number along with each item; start=1 makes it count from 1 for human-friendly numbering.

up = 0
for status in ["up", "down", "up"]:
    up = up + 1
print(up)
for i, ip in enumerate(["10.99.0.1", "10.99.0.2"], start=1):
    print(i, ip)
AUTO> python3 o4.py
3
1 10.99.0.1
2 10.99.0.2

The indentation error

for ip in ["10.99.0.1"]:
print(ip)
AUTO> python3 o5.py
  File "o5.py", line 2
    print(ip)
    ^
IndentationError: expected an indented block after 'for' statement on line 1

Python demands at least one indented line after the colon. Put 4 spaces in front of the print and the loop works.

Worked example. In the Lab 2 script the outer loop visits each router and an inner loop, indented 4 more spaces, visits each interface of that router. The inner body is at 8 spaces, and a line inside an if within it is at 12 spaces.

Common mistake. Mixing tabs and spaces, or indenting by different amounts. Use exactly 4 spaces per level everywhere. Also do not change the list while looping over it; build a new list instead.

Exam trap. Expect questions like "How many times does the body run?" Count the items. Remember range(1, 4) runs 3 times and does not include 4, and that the dictionary loop for k in d gives keys.

The loop that ran once

A script was meant to print "checked" for each of ten switches but printed it once. The print had been placed in the outer, un-indented position by mistake, after the loop. Indenting that line by 4 spaces brought it inside the loop and it printed ten times.

Lesson: indentation decides what is inside the loop.

"How do you loop over a dictionary and get both keys and values?"

Use for key, value in d.items():. Each pass unpacks one pair into the two variables. A plain for key in d: gives only keys, and d.values() gives only values.

Key takeaways

  • A for loop runs its indented body once per item.
  • The for line ends with a colon; the body is indented 4 spaces.
  • range(1, 4) gives 1, 2, 3; items() gives key and value.
  • A counter starts at 0 and adds 1; enumerate numbers the items.
06

Decisions: if / elif / else

When you read show ip interface brief you do not treat every line the same. An interface that is up is fine. One that is down needs a cable check. One that is administratively down was shut on purpose, so you leave it alone. You make a decision for each line. Python makes decisions with if, elif (else if) and else.

status? == "up": OK == "down": WARN else: ignore

Python tests the conditions from top to bottom and runs only the first branch that is true.

for status in ["up", "down", "administratively down"]:
    if status == "up":
        print(status, "-> OK")
    elif status == "down":
        print(status, "-> WARNING: check the cable")
    else:
        print(status, "-> shut on purpose, ignore")
AUTO> python3 c1.py
up -> OK
down -> WARNING: check the cable
administratively down -> shut on purpose, ignore
  • The if line ends with a colon; the lines under it are indented by 4 spaces.
  • Python checks if, then each elif, in order, and runs the first one whose condition is True. All others are skipped.
  • else has no condition. It catches everything that is left. You may leave it out.

Exact match matters

== on strings must match exactly. "administratively down" is a different string from "down", so it falls into else. This is a useful property here: a port you shut on purpose is not a cable fault.

print("administratively down" == "down")
print("down" in "administratively down")
print("down" == "down")
AUTO> python3 c2.py
False
True
True

The second line shows a trap: the word in asks "is it contained anywhere", so "down" in "administratively down" is True. Use == when you mean the whole value.

Combining conditions

Join conditions with and (both must be true), or (at least one) and not (flip). To act only when a list has something in it, test its length or simply the list itself: an empty list counts as False.

role = "core"
site = "Pune"
if role == "core" and site == "Pune":
    print("Pune core device")
down = []
if len(down) > 0:
    print("something is down")
else:
    print("nothing is down")
down.append("Gi0/2")
if down:
    print("now a list with items counts as True:", down)
if not down == []:
    print("same idea, written with not")
AUTO> python3 c3.py
Pune core device
nothing is down
now a list with items counts as True: ['Gi0/2']
same idea, written with not

Notice that nothing was printed for the first if len(down) > 0 branch, but the else branch ran. After append the list is no longer empty, so if down: is true.

The missing colon

status = "up"
if status == "up"
    print("OK")
AUTO> python3 c4.py
  File "c4.py", line 2
    if status == "up"
                   ^
SyntaxError: expected ':'

The arrow points just after the condition: Python expected a colon there.

Worked example. A compliance rule: "every core device must be reachable and not in Pune's maintenance list". In Python: if role == "core" and name not in maintenance:. The sentence and the code read almost the same.

Common mistake. Writing if status = "up": with one equals sign, forgetting the colon, or mis-indenting the branch. Also, a series of separate if lines is not the same as if / elif: separate ifs are all checked, elif stops at the first match.

Exam trap. Know the order of evaluation (top to bottom, first true branch wins), exact-string comparison, and that and binds tighter than or. An empty list, empty string and 0 are all falsy.

The alert storm

A monitoring script alerted on every interface that was not up. On a 48-port switch with many unused shut ports it sent 30 alerts a night. Changing the logic to alert only when the status was exactly down (a cable fault) and ignore administratively down reduced it to the two real problems.

Lesson: the exact condition you write decides whether alerts are useful or noise.

"What is the difference between if/elif/else and several separate if statements?"

In an if / elif / else chain, Python stops at the first true condition and skips the rest, so exactly one branch runs. With separate if statements every condition is tested independently, so several (or none) can run. Use elif when the cases are mutually exclusive.

Key takeaways

  • if runs its indented lines only when the condition is True; elif and else add alternatives.
  • The first true branch wins; the rest are skipped.
  • == on strings is an exact match; "in" tests containment.
  • An empty list counts as False; len(x) > 0 or just "if x:" tests for content.
07

Counting and collecting: Lab 1, the inventory

Almost every report is built from two moves inside a loop: count things, and collect things into a list. "How many core devices?" is counting. "Which devices are down?" is collecting. A third move, counting per group, uses a dictionary: "how many devices at each site?". This chapter teaches the three patterns and then walks through Lab 1 line by line.

Pattern 1 and 2: a counter and a collector

roles = ["core", "access", "core", "access", "access"]
core_count = 0
access_names = []
for i, role in enumerate(roles):
    if role == "core":
        core_count = core_count + 1
    else:
        access_names.append("device-" + str(i))
print(core_count)
print(access_names)
AUTO> python3 k1.py
2
['device-1', 'device-3', 'device-4']
  • Counter: start at 0 before the loop, add 1 inside the if. If you put core_count = 0 inside the loop, it resets every time.
  • Collector: start with an empty list [] before the loop and append inside.

Pattern 3: a dictionary as a counter

To count per group, use the group name as the key and the count as the value. The first time you meet a group it is not in the dictionary yet, so you create it with 0; then you add 1.

sites = ["Pune", "Pune", "Bengaluru", "Pune"]
counts = {}
for site in sites:
    if site not in counts:
        counts[site] = 0
    counts[site] = counts[site] + 1
print(counts)
AUTO> python3 k2.py
{'Pune': 3, 'Bengaluru': 1}

Read the three lines in the loop as a story: "If this site is new, start its count at 0. Then add one to its count." Without the first if you would get a KeyError on the first Pune.

Lab 1 walk-through

The file starts with a four-device dictionary and three report sections. Here are the lines you will touch, with their numbers, and what the script prints before you change anything. Section 2 prints every device under "Core devices", and the site counts are all zero: those are the two TODOs.

     8  }
    19      # TODO: print only when info["role"] is "core"
    20      print(f"  {name}")
    21  
    28      counts[site] = counts[site]  # TODO: add 1 here
    29  print("Per site:", counts)
AUTO> python3 inventory.py
All devices:
  PUN-CORE-01 -> 10.10.0.1
  PUN-ACC-01 -> 10.10.0.11
  BLR-CORE-01 -> 10.20.0.1
  BLR-ACC-01 -> 10.20.0.11
Total: 4
Core devices:
  PUN-CORE-01
  PUN-ACC-01
  BLR-CORE-01
  BLR-ACC-01
Per site: {'Pune': 0, 'Bengaluru': 0}

Step 1: add the Hyderabad switch

Insert one dictionary entry before the closing brace on line 8. Keep the four leading spaces so it lines up, and the comma at the end:

AUTO> insert inventory.py 8     "HYD-ACC-01": {"ip": "10.30.0.11", "role": "access", "site": "Hyderabad"},
AUTO> python3 inventory.py   (first 7 lines)
All devices:
  PUN-CORE-01 -> 10.10.0.1
  PUN-ACC-01 -> 10.10.0.11
  BLR-CORE-01 -> 10.20.0.1
  BLR-ACC-01 -> 10.20.0.11
  HYD-ACC-01 -> 10.30.0.11
Total: 5

Step 2: only core devices (TODO 1)

After the insert the TODO comment moved to line 20 and the print to line 21. Replace the comment with an if and indent the print one more level, so it only runs for core devices:

    19  for name, info in inventory.items():
    20      # TODO: print only when info["role"] is "core"
    21      print(f"  {name}")
    22  
    29      counts[site] = counts[site]  # TODO: add 1 here
    30  print("Per site:", counts)
AUTO> edit inventory.py 20     if info["role"] == "core":
AUTO> edit inventory.py 21         print(f"  {name}")
AUTO> python3 inventory.py
All devices:
  PUN-CORE-01 -> 10.10.0.1
  PUN-ACC-01 -> 10.10.0.11
  BLR-CORE-01 -> 10.20.0.1
  BLR-ACC-01 -> 10.20.0.11
  HYD-ACC-01 -> 10.30.0.11
Total: 5
Core devices:
  PUN-CORE-01
  BLR-CORE-01
Per site: {'Pune': 0, 'Bengaluru': 0, 'Hyderabad': 0}

Step 3: count per site (TODO 2)

The line counts[site] = counts[site] stores the old value back, which adds nothing. Make it add one, which is exactly pattern 3:

AUTO> edit inventory.py 29     counts[site] = counts[site] + 1
AUTO> python3 inventory.py
All devices:
  PUN-CORE-01 -> 10.10.0.1
  PUN-ACC-01 -> 10.10.0.11
  BLR-CORE-01 -> 10.20.0.1
  BLR-ACC-01 -> 10.20.0.11
  HYD-ACC-01 -> 10.30.0.11
Total: 5
Core devices:
  PUN-CORE-01
  BLR-CORE-01
Per site: {'Pune': 2, 'Bengaluru': 2, 'Hyderabad': 1}

The last line shows Hyderabad: 1: Hyderabad is the only site with a single device. Section 1 still lists every device because it has no filter, and that is correct.

Common mistake. Resetting the counter inside the loop, forgetting to create the dictionary key before adding to it, or mis-indenting the line you edit with edit (the leading spaces are part of the line). If the output looks wrong, run nl inventory.py and check the line numbers first.

Exam trap. Know the counting-in-a-dictionary pattern (if key not in d: d[key] = 0 then d[key] += 1) and the difference between putting x = 0 before and inside a loop.

Inventory by site for the budget

Finance asked how many devices each site runs, to plan support contracts. Instead of counting a spreadsheet by hand, an engineer added the per-site counting loop to the inventory script and sent the printed dictionary the same day. When a site opened, one new entry updated all the totals.

Lesson: counting patterns turn raw data into answers.

"How would you count devices per site from an inventory dictionary?"

Start with an empty dictionary, loop over the devices, and for each device read its site. If the site is not yet a key, set it to 0; then add 1. At the end the dictionary maps each site to its count. dict.get(site, 0) + 1 is a compact alternative.

Key takeaways

  • A counter starts at 0 before the loop and adds 1 inside it.
  • A collector starts as [] and uses append.
  • Per-group counts use a dictionary: create the key at 0, then add 1.
  • In Lab 1: add the device, filter the core devices, count per site.
08

Loop over routers: Lab 2, the up-interface check

Now put everything together on live (simulated) routers. The Mumbai site of Kaveri Foods has three routers: MUM-R1, MUM-R2 and MUM-R3. The script check_up.py logs in to each, reads show ip interface brief, and should count the interfaces that are up and warn about any that are down. The parts that log in and read are done; the counting and the warning are your TODOs. This is a full automation job: a list of devices, a loop, a parsed dictionary, a counter, a collector list and an if.

Reading the parsed table

Router output is text. The lab library nkparse turns that text table into a dictionary so you do not have to cut columns yourself: the key is the interface name and the value is a dictionary of its fields. This is what it gives for MUM-R3:

from nkmiko import ConnectHandler
from nkparse import parse

conn = ConnectHandler(device_type="nk_ios", host="10.99.0.3", username="netops", password="NK@2026")
output = conn.send_command("show ip interface brief")
conn.disconnect()
table = parse("show ip interface brief", output)["interface"]
print(type(table))
print(len(table))
print(table["GigabitEthernet0/2"])
for intf, info in table.items():
    print(intf, "->", info["status"])
AUTO> python3 pp.py
<class 'dict'>
4
{'ip_address': '10.3.2.1', 'interface_is_ok': 'YES', 'method': 'manual', 'status': 'down', 'protocol': 'down'}
GigabitEthernet0/0 -> up
GigabitEthernet0/1 -> up
GigabitEthernet0/2 -> down
GigabitEthernet0/3 -> administratively down

Three statuses appear: up, down (a real link problem: the cable or the far end) and administratively down (someone shut it on purpose). Your logic must treat them differently, exactly as in chapter 6.

The starting script

     5  ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]   # a list of router IPs
    17      for intf, info in table.items():
    18          pass        # TODO 1: if info["status"] == "up", add 1 to up
    19                      # TODO 2: elif info["status"] == "down", append intf to down
    20      print(f"{name}: {up} interfaces up")
    21      # TODO 3: if the down list is not empty, print a WARNING line
AUTO> python3 check_up.py
MUM-R1: 0 interfaces up
MUM-R2: 0 interfaces up
MUM-R3: 0 interfaces up
Check finished

The script runs and visits all three routers, but every count is 0 because the inner loop only says pass (do nothing). Line 5 is the router list; lines 15 and 16 create the counter up and the collector down for each router; line 17 starts the inner loop.

TODO 1: count the up interfaces

AUTO> edit check_up.py 18         if info["status"] == "up":
AUTO> edit check_up.py 19             up = up + 1
AUTO> python3 check_up.py
MUM-R1: 2 interfaces up
MUM-R2: 3 interfaces up
MUM-R3: 2 interfaces up
Check finished

Remember the indentation: the if is inside the inner loop (8 spaces) and the up = up + 1 is inside the if (12 spaces). The counts are 2, 3 and 2.

If you get the indentation wrong, for example by putting the increment at 8 spaces, Python tells you at once:

AUTO> python3 check_up.py
  File "check_up.py", line 19
    up = up + 1
    ^
IndentationError: expected an indented block after 'if' statement on line 18

TODO 2 and 3: collect down interfaces and warn

AUTO> insert check_up.py 20         elif info["status"] == "down":
AUTO> insert check_up.py 21             down.append(intf)
AUTO> insert check_up.py 24     if len(down) > 0:
AUTO> insert check_up.py 25         print(f"  WARNING {name}: down -> {down}")
AUTO> python3 check_up.py
MUM-R1: 2 interfaces up
MUM-R2: 3 interfaces up
MUM-R3: 2 interfaces up
  WARNING MUM-R3: down -> ['GigabitEthernet0/2']
Check finished

Now MUM-R3 is flagged: its GigabitEthernet0/2 is really down. MUM-R1 has a port that is administratively down (shut on purpose) and is not flagged, because the elif asks for exactly "down". Adding up the three counts (2 + 3 + 2) gives 7 interfaces up across the site.

The finished script

# check_up.py - visit every router and count the interfaces that are up
from nkmiko import ConnectHandler
from nkparse import parse

ROUTERS = ["10.99.0.1", "10.99.0.2", "10.99.0.3"]   # a list of router IPs

for ip in ROUTERS:
    conn = ConnectHandler(device_type="nk_ios", host=ip, username="netops", password="NK@2026")
    name = conn.find_prompt().replace("#", "")       # the hostname, e.g. MUM-R1
    output = conn.send_command("show ip interface brief")
    conn.disconnect()
    # parse() turns the text table into a dictionary of interfaces:
    # {"GigabitEthernet0/0": {"ip_address": "10.99.0.1", "status": "up", ...}, ...}
    table = parse("show ip interface brief", output)["interface"]
    up = 0          # counter: interfaces that are up
    down = []       # list: interfaces that are down (link/cable problem)
    for intf, info in table.items():
        if info["status"] == "up":
            up = up + 1
        elif info["status"] == "down":
            down.append(intf)
    print(f"{name}: {up} interfaces up")
    # TODO 3: if the down list is not empty, print a WARNING line
    if len(down) > 0:
        print(f"  WARNING {name}: down -> {down}")

print("Check finished")

Worked example. To check a fourth router later you add its management IP to ROUTERS. Nothing else changes. The same loop, counter and warning logic covers it.

Common mistake. Counting administratively down as a fault. A shut port is intentional, so warning on it creates noise. Also keep the connection open only as long as you need it: the script disconnects before it processes the text, which is fine because the text is already in the variable output.

Exam trap. Be able to read nested loop code and say what it prints. Also remember that parsed output is a dictionary keyed by interface name, and that the status field distinguishes up, down and administratively down.

Which router has the dead link?

After a power cut, the Mumbai NOC saw reduced traffic but no device alarms. An engineer ran the up-interface script across the site. Within seconds it printed one WARNING line naming MUM-R3 and GigabitEthernet0/2. A technician found a loose patch cable. Nothing else in the list was flagged, which saved a long manual search.

Lesson: a loop, a parsed table and a precise condition find a needle in a haystack.

"Describe how you would check all interfaces on many routers and report only real problems."

Keep a list of device IPs, loop over them, log in, run the interface summary command, parse it into a dictionary keyed by interface, loop over the interfaces and collect those whose status is exactly down (ignoring administratively down). Print a warning per device only when the collected list is not empty, and print a short summary at the end.

Key takeaways

  • Combine a device list, a loop, a parsed dictionary, a counter, a collector and an if.
  • Status up counts; exactly down is a fault; administratively down is intentional.
  • Indentation levels: 4 for the router loop, 8 for the interface loop, 12 inside the if.
  • Adding a router is one new IP in the list.
09

Summary and exam checklist

You can now hold many values in a list, label facts in a dictionary, nest them into an inventory, repeat work with a for loop and decide with if / elif / else. You combined those ideas to count, collect and flag problems across devices. This page is your revision sheet.

Can-do checklist

  • Create a list, read items by index (including -1), and use append, remove, in and len.
  • Create a dictionary, read, add and change keys, and read safely with get.
  • Read a nested lookup such as inventory["R1"]["ip"] and add a device entry.
  • Write a for loop over a list, over range() and over items().
  • Write if / elif / else with exact comparisons and with and / or.
  • Count with a counter, collect with a list and count per group with a dictionary.
  • Read IndexError, KeyError and IndentationError and fix them.

Mini glossary

List
Ordered values in [ ]; indexes start at 0.
Dictionary
Key to value pairs in { }.
Index / key
The position in a list / the label in a dictionary.
Nested
A container inside another container.
Loop
Repeats a block for every item.
Condition
An expression that is True or False.
Counter
A number that starts at 0 and grows inside a loop.
Falsy
Counts as False: empty list, empty string, 0.

Quick reference

TaskCode
First and last itemx[0] x[-1]
Add to a listx.append(item)
Read a key safelyd.get("key", "default")
Loop key and valuefor k, v in d.items():
Loop 1, 2, 3for n in range(1, 4):
Decideif a == b: ... elif ...: ... else:
Is a list empty?if x: or len(x) > 0
Add 1 to a countcount = count + 1

Most tested facts

  • Indexes start at 0; -1 is the last item; a slice excludes its end; range(1, 4) is 1, 2, 3.
  • d["missing"] is a KeyError; d.get("missing") returns None.
  • "x" in d tests keys, not values.
  • A loop body is indented 4 spaces and the line before it ends with a colon.
  • In if / elif / else the first true branch runs and the rest are skipped.
  • "administratively down" == "down" is False.

Next: "Python 3" covers functions, so you can name a piece of logic once and reuse it, and the later modules use these same lists and dictionaries when you read JSON from APIs.

From spreadsheet to script

A junior engineer took the team's inventory, wrote it as a dictionary of dictionaries and added a loop with one if. In an afternoon she produced the list of every core router with its IP address for the change window. Her manager asked for the same report for access switches; she changed one word, "core" to "access".

Lesson: small, general building blocks are easy to reuse.

"Walk me through a script that reports problems across many devices."

Store the devices in a list or dictionary, loop over them, connect and collect data, parse it into a dictionary, loop over the parsed fields with an if that tests exactly the failure condition, collect or count the matches, and print a concise report. Mention that the same loop handles any number of devices and that the logic is easy to test on one device first.

Key takeaways

  • Lists, dictionaries, loops and decisions are the core of every automation script.
  • Counting, collecting and per-group counting are the three report patterns.
  • Exact conditions (down, not administratively down) make useful alerts.
  • Next: functions in Python 3, then JSON, APIs and tools.
๐ŸŽ“ For educational purposes only โ€” all devices are simulationsTerms of UsePrivacy Policyยฉ 2026 Network Kings
CONFIG by Network Kings โ€” an educational IT simulation platform for learning purposes only. It is not Cisco IOS, Junos, FortiOS or PAN-OS and contains no Cisco, Juniper, Fortinet or Palo Alto Networks software. Cisco, IOS, CCNA, CCNP, Juniper, JNCIA, JNCIS, JNCIP, Fortinet, FortiGate, FortiOS, NSE, Palo Alto Networks, PAN-OS and PCNSE are trademarks of their respective owners. Network Kings is not affiliated with or endorsed by Cisco Systems, Inc., Juniper Networks, Inc., Fortinet, Inc. or Palo Alto Networks, Inc.