YAML and JSON for Network Inventories and Configs

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

  1. Warm-up. Given the dict {"hostname": "r1", "loopbacks": [0, 1, 100]}, print it as pretty JSON with 4‑space indentation.
  2. Round trip. Write that dict to r1.json, then read it back with json.load and print the number of loopbacks.
  3. YAML read. Create a sites.yaml with two sites, each having a region and a list of devices. Load it with safe_load and print each site name with its device count.
  4. Safety. Explain in one sentence why yaml.safe_load is preferred over yaml.load, then rewrite data = yaml.load(open("x.yaml")) to be both safe and to close the file properly.
  5. 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.

Leave a Reply