6.1 and 6.2 Python and JSON¶
Blueprint 6.1: Interpret basic Python components and scripts. Blueprint 6.2: Construct valid JSON-encoded files
- 6.1 Read a short Python script and say what it prints, what it sends, or why it fails: data types, variables, lists and dictionaries, conditionals, loops, functions, modules, exceptions, and the
requestsandjsonlibraries - 6.2 Write or fix a JSON document: the six value types, quoting rules, nesting, and how JSON maps to Python objects and to YANG-modelled payloads
The verbs are interpret and construct. You will not write a program from scratch on the exam. You will be shown 10 to 25 lines of Python and asked what happens, or shown a JSON body and asked which version is valid or what is wrong with it. Read code the way the interpreter does: top to bottom, one line at a time, keeping track of every variable.
Python building blocks¶
Data types¶
| Type | Example | Mutable | Notes |
|---|---|---|---|
str |
"GigabitEthernet1" |
No | Single or double quotes are both fine in Python |
int |
1500 |
No | Whole numbers |
float |
99.5 |
No | Decimal numbers |
bool |
True, False |
No | Capital first letter in Python |
None |
None |
No | "No value", the Python equivalent of JSON null |
list |
["Gi1", "Gi2"] |
Yes | Ordered, indexed from 0, duplicates allowed |
tuple |
("10.1.1.1", 22) |
No | Ordered, cannot be changed after creation |
dict |
{"name": "Gi1", "mtu": 1500} |
Yes | Key-value pairs, looked up by key |
set |
{10, 20, 30} |
Yes | Unordered, no duplicates |
type(x) returns the type of a value, and len(x) returns the number of items in a string, list, tuple, dict or set.
Indexing and slicing¶
Lists and strings are indexed from 0. Negative indexes count from the end, so -1 is the last item. A slice [start:stop] includes start and stops before stop.
vlans = [10, 20, 30, 40, 50]
print(vlans[0]) # 10
print(vlans[-1]) # 50
print(vlans[1:3]) # [20, 30]
print(len(vlans)) # 5
name = "GigabitEthernet1"
print(name[:2]) # Gi
Dictionaries¶
A dictionary maps keys to values. Network APIs return almost everything as nested dictionaries and lists, so this is the type you will read most.
device = {
"hostname": "R1",
"mgmt_ip": "192.168.100.1",
"interfaces": ["Gi1", "Gi2", "Gi3"],
"platform": {"model": "C8000V", "version": "17.12"},
}
print(device["hostname"]) # R1
print(device["platform"]["version"]) # 17.12
print(device["interfaces"][1]) # Gi2
print(device.get("serial", "unknown")) # unknown, no KeyError
device["site"] = "HQ" # add a new key
for key, value in device.items():
print(key, "=", value)
device["serial"] would raise a KeyError because the key does not exist. device.get("serial") returns None instead, or the default you pass as the second argument.
Conditionals, loops and functions¶
Python uses indentation to mark blocks. A line ending in a colon (if, for, while, def, try) starts a block, and every line in that block must be indented the same amount. Mixed indentation raises an IndentationError before anything runs.
def classify(util):
"""Return a label for an interface utilisation percentage."""
if util >= 90:
return "critical"
elif util >= 70:
return "warning"
else:
return "ok"
readings = {"Gi1": 45, "Gi2": 91, "Gi3": 70}
for intf, util in readings.items():
print(f"{intf}: {classify(util)}")
Output:
Gi3 is exactly 70, and >= includes 70, so it is a warning. Only the first branch that is true runs; once util >= 90 fails and util >= 70 succeeds, the else is skipped.
| Construct | What it does | Typical trap |
|---|---|---|
for x in items: |
Runs the block once per item | Iterating a dict gives its keys unless you use .items() or .values() |
for i in range(3): |
Counts 0, 1, 2 | range(3) never reaches 3 |
while cond: |
Repeats while cond is true |
Missing update to cond is an infinite loop |
break / continue |
Leave the loop / skip to the next item | break ends only the innermost loop |
def f(a, b=5): |
Function with a default argument | A function with no return returns None |
and, or, not |
Boolean logic | == compares values; = assigns |
Strings and f-strings¶
line = "GigabitEthernet1 192.168.100.1 YES manual up up"
fields = line.split() # split on any whitespace
print(fields[0], fields[1]) # GigabitEthernet1 192.168.100.1
print(fields[-1] == "up") # True
print(f"{fields[0]} has IP {fields[1]}")
print("Gi" in fields[0]) # True
print(line.startswith("Gig")) # True
An f-string (f"...{expr}...") evaluates each expression in braces and inserts the result. Without the leading f, the braces are printed literally.
Modules and imports¶
| Statement | Effect |
|---|---|
import json |
Use as json.loads(...) |
from json import loads |
Use as loads(...) directly |
import requests |
Third-party HTTP library, installed with pip |
from ncclient import manager |
NETCONF client library |
from netmiko import ConnectHandler |
SSH screen-scraping library for CLI devices |
if __name__ == "__main__": |
Run this block only when the file is executed directly, not when it is imported |
Exceptions¶
import json
raw = '{"hostname": "R1", "uptime": 86400}'
try:
data = json.loads(raw)
print(data["serial"])
except KeyError as err:
print("missing key:", err)
except json.JSONDecodeError:
print("not valid JSON")
finally:
print("done")
The JSON is valid, so json.loads succeeds, then data["serial"] raises KeyError. The first matching except runs and prints missing key: 'serial', and finally always runs. Output: missing key: 'serial' then done.
Common exceptions you may be asked to identify:
| Exception | Cause |
|---|---|
KeyError |
Dictionary key does not exist |
IndexError |
List index past the end |
TypeError |
Wrong type for the operation, such as "Gi" + 1 or indexing a list with a string |
NameError |
Variable used before it was assigned, or misspelled |
IndentationError / SyntaxError |
The file cannot be parsed, so nothing runs |
json.JSONDecodeError |
json.loads was given text that is not valid JSON |
requests.exceptions.ConnectionError |
No TCP connection to the server |
Exam trap
A SyntaxError or IndentationError stops the whole script before the first line executes, so earlier print statements produce no output. A runtime error such as KeyError happens only when that line is reached, so everything printed before it still appears.
The requests library¶
requests is how Python scripts talk to REST APIs such as Catalyst Center, SD-WAN Manager and RESTCONF.
import requests
BASE = "https://192.168.100.1/restconf/data"
HEADERS = {"Accept": "application/yang-data+json"}
resp = requests.get(
f"{BASE}/ietf-interfaces:interfaces",
headers=HEADERS,
auth=("admin", "Cisco123!"), # HTTP Basic authentication
verify=False, # lab only: skip certificate validation
timeout=10,
)
print(resp.status_code) # for example 200
resp.raise_for_status() # raise an exception for 4xx or 5xx
data = resp.json() # parse the JSON body into dicts and lists
| Part of the response | Type | What it holds |
|---|---|---|
resp.status_code |
int |
HTTP status, such as 200 or 404 |
resp.ok |
bool |
True for any status below 400 |
resp.text |
str |
Raw body as text |
resp.json() |
dict or list |
Body parsed as JSON; raises an error if the body is not JSON |
resp.headers |
dict-like | Response headers, case-insensitive keys |
| Call | Body argument | Sends |
|---|---|---|
requests.get(url, params={...}) |
none | Query string such as ?hostname=R1 |
requests.post(url, json=payload) |
json= |
Serialises a dict to JSON and sets Content-Type: application/json |
requests.post(url, data=payload) |
data= |
Form-encoded fields (or a raw string if you pass a string) |
requests.put(...), requests.patch(...), requests.delete(...) |
as above | The other HTTP methods |
Exam trap
json=payload and data=json.dumps(payload) both send a JSON body, but only json= sets the content type for you. RESTCONF needs Content-Type: application/yang-data+json, so a RESTCONF script sets that header explicitly in either case. data=payload with a dict sends form fields, which is exactly what SD-WAN Manager's login (j_security_check) expects and what a JSON API will reject.
JSON: the rules¶
JSON (RFC 8259) is a text format with six value types:
| JSON type | Example | Python type after json.loads() |
|---|---|---|
| object | {"name": "Gi1"} |
dict |
| array | [10, 20, 30] |
list |
| string | "up" |
str |
| number | 1500, 99.5, -3, 1e3 |
int or float |
| boolean | true, false |
True, False |
| null | null |
None |
The rules that exam questions test:
- Keys are always strings in double quotes.
{name: "R1"}and{'name': 'R1'}are invalid. - Strings use double quotes only.
true,falseandnullare lowercase and unquoted."true"is a string, not a boolean.- Members and array items are separated by commas, with no trailing comma after the last one.
- No comments of any kind.
- Numbers have no leading zeros (
010is invalid), no hex, no+sign, and no quotes (quoting makes them strings). - Whitespace and line breaks between tokens do not matter.
- Duplicate keys in one object are discouraged by the RFC and their handling is unpredictable, so treat them as an error.
Mapping Python to JSON¶
import json
payload = {
"hostname": "R1",
"vlans": (10, 20), # tuple becomes a JSON array
"enabled": True, # becomes true
"location": None, # becomes null
}
text = json.dumps(payload, indent=2)
print(text)
back = json.loads(text)
print(type(back["vlans"]).__name__) # list, not tuple
Output:
| Function | Direction | Mnemonic |
|---|---|---|
json.loads(s) |
JSON string to Python | load string |
json.dumps(obj) |
Python to JSON string | dump string |
json.load(f) |
JSON file to Python | |
json.dump(obj, f) |
Python to JSON file |
A Python set cannot be serialised (TypeError: Object of type set is not JSON serializable), and a tuple comes back as a list.
graph LR
P["Python dict and list"] -->|"json.dumps"| S["JSON text"]
S -->|"json.loads"| P
S -->|"HTTP body"| API["REST API"]
API -->|"HTTP body"| S
JSON in RESTCONF payloads¶
RESTCONF JSON follows RFC 7951, which adds a few rules on top of plain JSON:
- The top-level member name is module-qualified:
"ietf-interfaces:interfaces". A child from a different module (an augmentation) is also prefixed, as in"ietf-ip:ipv4"inside anietf-interfacesinterface. - A YANG
containeris a JSON object, a YANGlistis a JSON array of objects, aleaf-listis an array of values, and aleafis a single member. - Identity values carry the module name:
"type": "iana-if-type:softwareLoopback". - 64-bit integer and
decimal64values are encoded as JSON strings so they survive parsers that use floating point.
Here is a valid payload that creates Loopback100 with the IETF model:
{
"ietf-interfaces:interface": {
"name": "Loopback100",
"description": "Created by RESTCONF",
"type": "iana-if-type:softwareLoopback",
"enabled": true,
"ietf-ip:ipv4": {
"address": [
{
"ip": "10.100.100.1",
"netmask": "255.255.255.255"
}
]
}
}
}
address is a YANG list, so it is an array even with one entry. enabled is a boolean, so it is unquoted.
Exam trap
Questions that ask "which payload is valid" usually differ by one character: a single quote, a capitalised True, a trailing comma, a missing comma between members, or a list written as an object. Check those five things in that order and you will find the fault quickly.
Reading a whole script¶
This is the kind of script you will be asked to interpret. It uses a mocked response so it runs without a device.
import json
# Body of GET /restconf/data/ietf-interfaces:interfaces (trimmed)
body = """
{
"ietf-interfaces:interfaces": {
"interface": [
{"name": "GigabitEthernet1", "enabled": true,
"ietf-ip:ipv4": {"address": [{"ip": "192.168.100.1", "netmask": "255.255.255.0"}]}},
{"name": "GigabitEthernet2", "enabled": false, "ietf-ip:ipv4": {}},
{"name": "Loopback0", "enabled": true,
"ietf-ip:ipv4": {"address": [{"ip": "10.255.255.1", "netmask": "255.255.255.255"}]}}
]
}
}
"""
data = json.loads(body)
up_count = 0
for intf in data["ietf-interfaces:interfaces"]["interface"]:
addrs = intf["ietf-ip:ipv4"].get("address", [])
ip = addrs[0]["ip"] if addrs else "unassigned"
state = "up" if intf["enabled"] else "shutdown"
if intf["enabled"]:
up_count += 1
print(f"{intf['name']:<18}{ip:<16}{state}")
print("enabled:", up_count)
Output:
GigabitEthernet1 192.168.100.1 up
GigabitEthernet2 unassigned shutdown
Loopback0 10.255.255.1 up
enabled: 2
Walk through it the way an exam question expects:
json.loadsturns the string into a dict. The interfaces are a list under["ietf-interfaces:interfaces"]["interface"].- Gi2 has an empty
ietf-ip:ipv4object, so.get("address", [])returns an empty list, and the conditional expression picks"unassigned". Usingintf["ietf-ip:ipv4"]["address"]instead would raiseKeyErroron Gi2. {ip:<16}left-aligns the value in a 16-character column. Formatting details like this are rarely tested, but they explain the spacing.
graph TB
R["dict: top level"] --> I["key ietf-interfaces:interfaces, a dict"]
I --> L["key interface, a list"]
L --> E0["index 0: dict for GigabitEthernet1"]
L --> E1["index 1: dict for GigabitEthernet2"]
E0 --> V4["key ietf-ip:ipv4, a dict"]
V4 --> A["key address, a list"]
A --> A0["index 0: dict with ip and netmask"]
Lab: parse RESTCONF output with Python¶
Topology: the same two nodes as Sim 12: AUTO (Ubuntu, 192.168.100.50) connected to R1 (Catalyst 8000v, Gi1 192.168.100.1/24). IOSv does not support RESTCONF.
graph LR
AUTO["AUTO Ubuntu<br/>192.168.100.50"] -- "eth0 192.168.100.0/24 Gi1" --- R1["R1 C8000v<br/>192.168.100.1"]
-
Enable RESTCONF on R1:
-
Verify the RESTCONF process. Wait a minute after
restconffor the processes to start.R1# show platform software yang-management process confd : Running nesd : Running syncfd : Running ncsshd : Not Running dmiauthd : Running nginx : Running ndbmand : Running pubd : Runningnginxis the HTTPS front end for RESTCONF and must beRunning.ncsshdis the NETCONF SSH server, which stays down until you also configurenetconf-yang. The exact process list varies a little between releases. -
On AUTO, save the interface data to a file:
-
Save the script from "Reading a whole script" as
intf.py, replace thebody = """..."""block withbody = open("intf.json").read(), and runpython3 intf.py. You should see one line per interface, including Loopback0 with 10.255.255.1. - Break it on purpose: add a new interface with no IP address (
interface Loopback5), fetch the file again, and change the script to useintf["ietf-ip:ipv4"]["address"]. Confirm you get aKeyError, then restore the.get()version. - Write a JSON file by hand that creates Loopback100 (use the payload above), then check it with
python3 -m json.tool lb100.json. A syntax error is reported with the line and column. Keep the file for the response codes lab.
Quick check¶
What does print(len({'a': [1, 2, 3], 'b': None})) print?
2. len() of a dictionary counts its keys, not the items inside the values.
A JSON body contains the member vlan with the unquoted value 010. Is that valid JSON?
No. JSON numbers cannot have leading zeros. Use 10, or "010" if a string is intended.
A script calls requests.post(url, data=payload) where payload is a dict, and a JSON API returns 400. What is the likely fix?
Use json=payload (or data=json.dumps(payload) with a JSON Content-Type header). data= with a dict sends form-encoded fields, not JSON.
References¶
- Python 3 tutorial and the json module documentation
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format
- RFC 7951: JSON Encoding of Data Modeled with YANG
- Cisco DevNet: learning labs on Python, REST APIs and RESTCONF
- Programmability Configuration Guide, Cisco IOS XE (search on cisco.com for your release)
- Cisco Live On-Demand Library: search "Python for network engineers" and "RESTCONF"
Unlock the full question banks and cram sheets
A free account opens every question bank, the timed practice exams, the printable cram sheets, all the simulations and the lab files. Without one you see a 5-question sample of each.
No card needed. Study tips by email are optional and off unless you tick the box. Your address is never sold or shared. Privacy policy ยท Terms
Signed in as . Your account
You're on the free account, so lessons after the first module, question banks, exams and cram sheets show a sample. See Track Pass and All-Access
Also from NetManager Systems: NetManager, network monitoring for multi-vendor networks. Try the NetManager, NetDiag, PCAP Analyzer and OpsDash demos: every new account gets a free 7-day read-only login to each.