Mental model
Network engineering used to be SSH, command, screen-scrape, repeat. That doesn’t scale past a handful of devices. Python is the language of choice for replacing that workflow with code:
- Easy syntax — closer to English than other languages
- Massive ecosystem of network libraries
- Runs on Windows, macOS, Linux, your laptop, a server, in CI
- Same skills transfer to other automation (servers, cloud, security)
You don’t need to be a software engineer. You need to write small, useful scripts. 50 lines that update a thousand switches’ SNMP community string in 30 seconds is real Python value.
The four libraries you’ll actually use
| Library | Job | When to use |
|---|---|---|
| Netmiko | SSH to network devices, send commands | Quick scripts, multi-vendor SSH |
| NAPALM | Vendor-agnostic operations (get facts, push config, rollback) | When you need cross-vendor compatibility |
| Nornir | Parallel runner with built-in inventory | Running anything against many devices fast |
| requests | HTTP / REST APIs | Modern APIs (Meraki, DNA Center, RESTCONF) |
Plus paramiko (lower-level SSH — Netmiko uses it under the hood) and ncclient (NETCONF — see NETCONF & YANG topic).
Hello world — read interface status from one device
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "10.0.0.1",
"username": "admin",
"password": "cisco123",
}
with ConnectHandler(**device) as conn:
output = conn.send_command("show ip interface brief")
print(output)
That’s a working network-automation script. Eight lines. Replace with send_config_set(["interface gi0/0", "description WAN-link"]) to push config.
Loop over many devices
from netmiko import ConnectHandler
devices = [
{"device_type": "cisco_ios", "host": "10.0.0.1", "username": "admin", "password": "x"},
{"device_type": "cisco_ios", "host": "10.0.0.2", "username": "admin", "password": "x"},
{"device_type": "cisco_ios", "host": "10.0.0.3", "username": "admin", "password": "x"},
]
for dev in devices:
with ConnectHandler(**dev) as conn:
print(f"--- {dev['host']} ---")
print(conn.send_command("show version | i Cisco IOS"))
Run it: takes ~30 seconds for 100 devices in serial. Use Nornir or concurrent.futures to parallelize down to ~3 seconds.
NAPALM — vendor-agnostic
NAPALM gives you a consistent API across Cisco IOS, NX-OS, Juniper, Arista, and others. Same code, different platform:
from napalm import get_network_driver
driver = get_network_driver("ios")
device = driver(hostname="10.0.0.1", username="admin", password="x")
device.open()
facts = device.get_facts()
print(facts["model"], facts["os_version"])
interfaces = device.get_interfaces()
for name, data in interfaces.items():
print(f"{name}: up={data['is_up']}")
device.close()
Switch "ios" to "junos" and the script works against a Juniper router. NAPALM normalizes the differences.
REST API — Cisco Meraki example
import requests
api_key = "your-api-key"
org_id = "your-org-id"
resp = requests.get(
f"https://api.meraki.com/api/v1/organizations/{org_id}/networks",
headers={"X-Cisco-Meraki-API-Key": api_key},
)
networks = resp.json()
for net in networks:
print(net["id"], net["name"])
Modern Cisco platforms (Meraki, DNA Center, ISE, Webex) all expose REST APIs. See REST APIs for Network Engineers for the deeper dive.
Parallel execution with Nornir
Nornir is Python-native parallelism + inventory + plugin system. Same idea as Ansible but feels native (no YAML files for tasks).
from nornir import InitNornir
from nornir_netmiko import netmiko_send_command
nr = InitNornir(config_file="config.yaml")
result = nr.run(task=netmiko_send_command, command_string="show ip int brief")
for host, output in result.items():
print(f"--- {host} ---")
print(output[0].result)
InitNornir reads hosts.yaml and groups.yaml (Ansible-style inventory). The .run() call executes in parallel — default 20 workers, configurable.
Practical patterns you’ll use
1. Bulk config audit
for dev in inventory:
output = ssh.send_command("show running-config | i ^username")
if "admin" not in output:
print(f"ALERT: {dev['host']} missing admin user")
2. Backup all configs to git
for dev in inventory:
config = ssh.send_command("show running-config")
open(f"backups/{dev['host']}.cfg", "w").write(config)
# then: git add . && git commit -m "Daily backup"
3. Templated config push
from jinja2 import Template
template = Template("""
interface {{ interface }}
description {{ description }}
ip address {{ ip }} {{ mask }}
no shutdown
""")
config = template.render(
interface="Gi0/0",
description="WAN to ISP-A",
ip="203.0.113.1",
mask="255.255.255.252",
).splitlines()
ssh.send_config_set(config)
Jinja2 for templates is universal — Ansible uses the same templating engine.
Common mistakes
- Hardcoding credentials. Use environment variables or a secrets file you exclude from git:
import os
password = os.environ["NETWORK_PASSWORD"]
- No error handling. SSH connections fail constantly (timeouts, wrong creds, device down). Wrap in
try/except:
from netmiko.exceptions import NetMikoTimeoutException
try:
with ConnectHandler(**dev) as conn:
...
except NetMikoTimeoutException:
print(f"Could not reach {dev['host']}")
Pushing config without a backup or rollback plan. Backup the running config first, push the change, verify the result, restore from backup if needed.
Running on all 500 devices on the first try. Start with one. Then ten. Then all of them. Mistakes at scale are expensive.
Mixing Python 2 and Python 3. Python 2 is end-of-life since 2020. Use Python 3.10+ for modern syntax, type hints, and library compatibility.
Not using virtual environments.
pip installsystem-wide pollutes your machine. Always:
python3 -m venv venv
source venv/bin/activate
pip install netmiko napalm nornir requests
- Treating output as freeform text forever. Plain
send_command()returns text you have to regex. Use NAPALM’sget_interfaces()or device APIs that return structured JSON when possible.
Lab to try tonight
- Spin up a Cisco DevNet sandbox or two Cisco devices in CML.
pip install netmikoin a virtualenv.- Write a script that SSHes to one device and prints
show version. Get it working with one device. - Add a second device. Loop over both.
- Send a config change (e.g.
description teston a loopback). Verify it stuck via SSH. - Add
try/exceptfor connection failures. - Switch to NAPALM and use
get_facts(). Compare vs the raw text output. - Bonus: use Jinja2 to template a multi-line config from a dictionary of variables. Push it.
Cheat strip
| Library | What it does |
|---|---|
| Netmiko | SSH wrapper for multi-vendor — start here |
| NAPALM | Vendor-agnostic ops — same code, different platforms |
| Nornir | Inventory + parallel runner — pure Python alternative to Ansible |
| requests | HTTP / REST APIs |
| ncclient | NETCONF (XML over SSH) |
| paramiko | Low-level SSH (used by Netmiko under the hood) |
| Jinja2 | Config templating |
| Virtualenv | Isolated Python environment — always use one |
| Environment vars | For secrets — never hardcode passwords |
try/except | Network operations fail. Wrap them. |
