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. |