Netmiko sends commands and reads text. That is perfect when each platform’s CLI is familiar. But consider asking “give me the interfaces” and getting the same structured answer whether the device is a Cisco IOS-XE router, an Arista switch, or a Juniper firewall — without learning three command syntaxes. That is NAPALM: Network Automation and Programmability Abstraction Layer with Multivendor support. One Python API, many vendors, structured data out, and — the headline feature — safe configuration changes with diffs and rollback.
NAPALM sits one level of abstraction above Netmiko. It is the right tool for a multi-vendor network, or for config changes that need to be previewed and undone.
Install and the Driver Concept
pip install napalm
NAPALM uses drivers — one per platform. Request a driver by name, then open a connection:
from napalm import get_network_driver
driver = get_network_driver("ios") # ios, eos, junos, nxos, iosxr
device = driver(
hostname="192.168.1.1",
username="admin",
password="cisco123",
optional_args={"secret": "enablepass"},
)
device.open()
print("Connected")
device.close()
Supported drivers cover the big four: ios, eos (Arista), junos (Juniper), nxos (Nexus), plus iosxr. The point is that everything below works identically across all of them.
Getters: Structured Facts, Same Shape Everywhere
NAPALM’s “getters” are read methods that return normalized dictionaries. The same call returns the same structure regardless of vendor:
device.open()
facts = device.get_facts()
print(facts["hostname"], facts["model"], facts["os_version"])
print(facts["uptime"], "seconds")
interfaces = device.get_interfaces()
for name, data in interfaces.items():
state = "up" if data["is_up"] else "down"
print(f"{name}: {state}, MAC {data['mac_address']}")
device.close()
Run the exact same code against an Arista box (driver eos) and the keys are identical — is_up, mac_address, description. That normalization is the whole value proposition. Other common getters: get_interfaces_ip(), get_bgp_neighbors(), get_lldp_neighbors(), get_arp_table(), get_environment().
The Killer Feature: Config Diff and Rollback
This is why people adopt NAPALM. Load a candidate configuration, ask NAPALM what would change, and only then commit — or discard. On platforms without native candidate configs, NAPALM emulates the workflow automatically.
device.open()
# Stage a config change (merge = add to running config)
device.load_merge_candidate(config="ntp server 10.0.0.1\nlogging host 10.0.0.50")
# Preview EXACTLY what will change - before touching anything
diff = device.compare_config()
print(diff)
# +ntp server 10.0.0.1
# +logging host 10.0.0.50
if diff:
device.commit_config() # apply it
print("Committed")
else:
device.discard_config() # nothing to do
print("No changes")
device.close()
That flow is worth reading twice, because it is a different mindset from Netmiko. Nothing gets pushed blindly. Stage the change, see the diff, decide. For change windows and peer review, compare_config() is gold — the exact diff drops straight into the change ticket.
Merge vs. Replace
Two ways to load config, and the difference matters enormously:
load_merge_candidate()— adds new lines to the existing config. Like typing commands at the CLI. Safe and incremental.load_replace_candidate()— replaces the entire running config with the supplied file. Powerful for enforcing a golden config, dangerous if that file is incomplete. The diff preview is the seatbelt here.
# Enforce a full golden config from a file, but LOOK before leaping
device.load_replace_candidate(filename="golden_r1.cfg")
print(device.compare_config()) # review every add AND removal
# commit only if the diff is what you expect
Rollback
When a committed change turns out wrong, NAPALM can roll back to the previous config on platforms that support it:
device.rollback() # revert the last commit
Cisco Context: A Multi-Vendor Health Snapshot
Because getters are normalized, one function can inventory a mixed fleet — IOS and EOS side by side — with zero per-vendor branching:
from napalm import get_network_driver
fleet = [
{"driver": "ios", "host": "192.168.1.1"},
{"driver": "eos", "host": "192.168.1.10"},
]
USER, PW = "admin", "cisco123"
for entry in fleet:
drv = get_network_driver(entry["driver"])
dev = drv(hostname=entry["host"], username=USER, password=PW)
try:
dev.open()
f = dev.get_facts()
down = [n for n, d in dev.get_interfaces().items() if not d["is_up"]]
print(f"{f['hostname']:12} {f['model']:14} up {f['uptime']//86400}d, "
f"{len(down)} interfaces down")
except Exception as e:
print(f"{entry['host']}: ERROR {e}")
finally:
dev.close()
That is the goal: one report across vendors, written once. No if device_type == ... anywhere.
Netmiko or NAPALM? A Quick Rule
Reach for Netmiko when the job needs arbitrary CLI commands, vendor-specific features, or fine control over exactly what is sent. Reach for NAPALM for normalized facts across vendors, or safe config changes with diff and rollback. Many shops use both — NAPALM for getters and config management, Netmiko for the one weird command NAPALM does not expose. They are friends, not rivals.
Exercises
- Warm-up. Open a connection with the
iosdriver, print the hostname and model fromget_facts(), and close cleanly withtry/finally. - Interface report. Use
get_interfaces()to print a table of interface name, up/down, and description. - Safe change. Stage an NTP server line with
load_merge_candidate, print the diff, and commit only if the diff is non-empty (otherwise discard). - BGP check. Use
get_bgp_neighbors()to print each neighbor IP and whether the session is up. - Challenge. Write
enforce_golden(host, driver, golden_file)that loads a replace candidate from a file, prints the diff, and — only if the diff contains no removal lines (no lines starting with-) — commits; otherwise it discards and warns that the change would remove config. This is a guardrail against an incomplete golden file wiping a device.
Answers
Show answers
1. Warm-up
from napalm import get_network_driver
dev = get_network_driver("ios")(
hostname="192.168.1.1", username="admin", password="cisco123")
try:
dev.open()
f = dev.get_facts()
print(f["hostname"], f["model"])
finally:
dev.close()
2. Interface report
dev.open()
for name, d in dev.get_interfaces().items():
print(f"{name:22} {'up' if d['is_up'] else 'down':5} {d['description']}")
dev.close()
3. Safe change
dev.open()
dev.load_merge_candidate(config="ntp server 10.0.0.1")
diff = dev.compare_config()
print(diff)
if diff:
dev.commit_config()
else:
dev.discard_config()
dev.close()
4. BGP check
dev.open()
bgp = dev.get_bgp_neighbors()
for vrf, data in bgp.items():
for peer, info in data["peers"].items():
print(f"{peer}: {'up' if info['is_up'] else 'down'}")
dev.close()
5. Challenge
from napalm import get_network_driver
def enforce_golden(host, driver, golden_file):
dev = get_network_driver(driver)(
hostname=host, username="admin", password="cisco123")
try:
dev.open()
dev.load_replace_candidate(filename=golden_file)
diff = dev.compare_config()
removals = [l for l in diff.splitlines() if l.startswith("-")]
if removals:
print(f"{host}: REFUSING — change would remove "
f"{len(removals)} lines:")
print("\n".join(removals))
dev.discard_config()
else:
print(f"{host}: safe additive change, committing")
dev.commit_config()
finally:
dev.close()
Inspecting the diff text for lines starting with - turns NAPALM’s preview into an automated guardrail. This is the kind of safety net that makes a change window restful — and a fitting end to a week of talking to real devices.
This is Day 14 of the 21‑post Python for Network Engineers series.
Previously: Multi-Vendor SSH with Netmiko. Coming next week, the series moves up the stack — YAML and JSON for inventories and configs, then Jinja2 templating, REST APIs, NETCONF, TextFSM, and two end-to-end mini-projects.