Week 2 ended with scripts that connect to devices — but every one of them had the inventory hardcoded in a Python dict. That does not scale. Real automation keeps the data (which devices, which VLANs, which settings) separate from the code (what to do with them). The two formats that carry that data across the entire network industry are YAML and JSON. Ansible inventories are YAML. NETCONF and REST APIs speak JSON. Nornir, Netbox exports, CI pipelines — all YAML or JSON. Today is about reading and writing both from Python, cleanly.
This is Day 15 of the 21‑post Python for Network Engineers series, and the start of Week 3 — APIs, structured data, and two end‑to‑end mini‑projects.
JSON: the machine format
JSON is what APIs return. Python’s built‑in json module converts between JSON text and Python objects with four functions. Two work on strings, two work on files:
import json
# A Python dict...
device = {"host": "10.0.0.1", "vlans": [10, 20, 30], "enabled": True}
# ...to a JSON string (dumps = "dump string")
text = json.dumps(device, indent=2)
print(text)
# {
# "host": "10.0.0.1",
# "vlans": [10, 20, 30],
# "enabled": true
# }
# ...and back (loads = "load string")
back = json.loads(text)
print(back["vlans"][0]) # 10
Note the type mapping: Python True becomes JSON true, None becomes null, a dict becomes an object, a list becomes an array. The round trip is lossless for these basic types. The indent=2 argument pretty‑prints; leave it off for compact machine‑to‑machine output.
Reading and writing JSON files
import json
from pathlib import Path
# Write
data = {"routers": ["r1", "r2"], "count": 2}
with open("inventory.json", "w") as f:
json.dump(data, f, indent=2) # dump (no 's') writes to a file
# Read
with open("inventory.json") as f:
loaded = json.load(f) # load (no 's') reads from a file
print(loaded["count"]) # 2
The naming is worth memorizing because it trips people up: dumps/loads (with the s) work on strings; dump/load work on file objects. Same functions, string vs. file.
YAML: the human format
JSON is precise but noisy — all those braces and quotes are painful to hand‑edit. YAML carries the same data model (scalars, lists, dicts) with indentation‑based syntax that reads like an outline. Compare the same inventory:
# inventory.yaml
routers:
r1:
host: 10.0.0.1
device_type: cisco_ios
site: hq
r2:
host: 10.0.0.2
device_type: cisco_ios
site: branch
No braces, no quotes on simple strings, comments allowed with #. This is why humans write inventories in YAML and machines exchange them in JSON. YAML is not in the standard library — install PyYAML (it was in the Week 1 pip install list):
pip install pyyaml
Loading YAML into Python
import yaml
with open("inventory.yaml") as f:
inv = yaml.safe_load(f) # ALWAYS safe_load, never load
# It's now a normal Python dict
for name, attrs in inv["routers"].items():
print(f"{name}: {attrs['host']} ({attrs['site']})")
# r1: 10.0.0.1 (hq)
# r2: 10.0.0.2 (branch)
Critical safety rule: always use yaml.safe_load(), never plain yaml.load(). The unsafe version can execute arbitrary Python objects embedded in the file — a genuine remote‑code‑execution risk if the YAML comes from anywhere untrusted. safe_load only builds plain data types. There is no downside for inventory files, so make it a reflex.
Writing YAML back out
import yaml
data = {
"vlans": {
10: {"name": "USERS"},
20: {"name": "VOICE"},
}
}
with open("vlans.yaml", "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
default_flow_style=False forces the readable block style (one item per line) instead of inline braces. sort_keys=False preserves insertion order, which matters when the output is meant to be read by a person.
Cisco Context: Inventory‑Driven Config, No Hardcoding
Here is the whole point of the day — a YAML inventory feeding straight into the Netmiko connection pattern from Week 2. The code never changes; only the YAML does.
# devices.yaml
- device_type: cisco_ios
host: 192.168.1.1
username: admin
password: cisco123
- device_type: cisco_ios
host: 192.168.1.2
username: admin
password: cisco123
import yaml
# from netmiko import ConnectHandler # Week 2
with open("devices.yaml") as f:
devices = yaml.safe_load(f)
for dev in devices:
print(f"Would connect to {dev['host']} as {dev['username']}")
# with ConnectHandler(**dev) as conn:
# print(conn.send_command("show version"))
Because safe_load returns a list of dicts whose keys already match Netmiko’s parameter names, ConnectHandler(**dev) just works — the ** unpacking from Day 6 paying off. Adding a device to the fleet is now a three‑line edit to a YAML file, not a code change. That separation is the foundation of every serious automation framework.
Converting Between the Two
A frequent task: an API returns JSON, and the goal is a human‑readable YAML copy (or vice versa). Because both load into the same Python data model, conversion is two lines:
import json, yaml
json_text = '{"host": "10.0.0.1", "vlans": [10, 20]}'
data = json.loads(json_text) # JSON -> Python
print(yaml.safe_dump(data, default_flow_style=False)) # Python -> YAML
# host: 10.0.0.1
# vlans:
# - 10
# - 20
Exercises
- Warm-up. Given the dict
{"hostname": "r1", "loopbacks": [0, 1, 100]}, print it as pretty JSON with 4‑space indentation. - Round trip. Write that dict to
r1.json, then read it back withjson.loadand print the number of loopbacks. - YAML read. Create a
sites.yamlwith two sites, each having aregionand a list ofdevices. Load it withsafe_loadand print each site name with its device count. - Safety. Explain in one sentence why
yaml.safe_loadis preferred overyaml.load, then rewritedata = yaml.load(open("x.yaml"))to be both safe and to close the file properly. - Challenge. Write
json_to_yaml(json_path, yaml_path)that reads a JSON file and writes an equivalent YAML file in block style with keys unsorted. Test it on a small inventory and confirm the YAML round‑trips back to an identical Python object.
Answers
Show answers
1. Warm-up
import json
d = {"hostname": "r1", "loopbacks": [0, 1, 100]}
print(json.dumps(d, indent=4))
2. Round trip
import json
with open("r1.json", "w") as f:
json.dump(d, f, indent=2)
with open("r1.json") as f:
loaded = json.load(f)
print(len(loaded["loopbacks"])) # 3
3. YAML read
# sites.yaml
hq:
region: west
devices: [r1, r2, sw1]
branch:
region: east
devices: [r3]
import yaml
with open("sites.yaml") as f:
sites = yaml.safe_load(f)
for name, attrs in sites.items():
print(f"{name}: {len(attrs['devices'])} devices")
# hq: 3 devices
# branch: 1 devices
4. Safety
safe_load only constructs plain data types, whereas load can instantiate arbitrary Python objects encoded in the file — an arbitrary‑code‑execution risk with untrusted input.
import yaml
with open("x.yaml") as f:
data = yaml.safe_load(f)
5. Challenge
import json, yaml
def json_to_yaml(json_path, yaml_path):
with open(json_path) as f:
data = json.load(f)
with open(yaml_path, "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
# verify the round trip
json_to_yaml("inventory.json", "inventory.yaml")
with open("inventory.json") as a, open("inventory.yaml") as b:
assert json.load(a) == yaml.safe_load(b)
print("round trip OK")
The assert confirms both formats decode to the same Python object — the practical proof that YAML and JSON share one data model.
Previously: NAPALM. Coming tomorrow — Jinja2 templates: generating a hundred switch configs from one template and a YAML inventory.