NAPALM vs Netmiko: Vendor-Agnostic Config vs Raw CLI, and When You Want Both
Two libraries that look like alternatives, but are not
Most network engineers learn Netmiko first, hit a config-management problem, hear about NAPALM, and assume the choice is “which one do I use”. That framing is wrong. NAPALM and Netmiko solve overlapping but different problems. Netmiko gives you SSH with platform-aware quirk handling. NAPALM gives you a vendor-agnostic API for configuration operations and a fixed library of “getters” for common operational state. Both have their place; production tooling almost always uses both.
This post walks through what each one is actually for, the same job done in both, the cases where one clearly wins, and the hybrid pattern I have ended up using in every serious project.
What Netmiko is for
Netmiko is the SSH layer. It connects, handles enable mode and configuration mode, deals with paging, and gives you methods to send commands and capture responses. It does not know what show ip route means; it just knows that show ip route is a command, the device will return text, and Netmiko’s job is to make that round-trip work reliably across platforms.
from netmiko import ConnectHandler
with ConnectHandler(device_type="cisco_ios", host="10.0.0.1",
username="alice", password="redacted") as conn:
conn.enable()
output = conn.send_command("show ip route")
conn.send_config_set(["interface gi0/1", " description Audited"])
Strengths: every command works, including weird ones, vendor-specific ones, and ones the library has never heard of. As long as you can type it into a terminal, Netmiko can run it. Platform support is broad — IOS, XE, XR, NX-OS, ASA, Junos, EOS, FortiOS, Linux, dozens more.
Weaknesses: you are dealing in text. You write your own logic for everything beyond “send command, get text back”. Config changes are unstructured strings, with no built-in concept of “what did this device’s config look like before, and what does it look like after, and was the change what I intended”.
What NAPALM is for
NAPALM (Network Automation and Programmability Abstraction Layer with Multivendor support) sits at a higher level. It defines a standard API — get_facts(), get_interfaces(), get_bgp_neighbors(), load_merge_candidate(), compare_config(), commit_config(), rollback() — and provides driver implementations for IOS, IOS-XR, NX-OS, EOS, and Junos. The point of NAPALM is that the same Python code works across vendors, with the driver handling the platform-specific details.
from napalm import get_network_driver
driver = get_network_driver("ios")
with driver(hostname="10.0.0.1", username="alice", password="redacted") as device:
facts = device.get_facts() # vendor-agnostic dict
interfaces = device.get_interfaces() # vendor-agnostic dict
neighbors = device.get_bgp_neighbors()
device.load_merge_candidate(filename="snippet.cfg")
diff = device.compare_config()
if diff:
print(diff)
device.commit_config()
# or: device.discard_config()
Strengths:
- Same code across vendors. The contract is the same whether the device is IOS, NX-OS, or EOS. The driver hides the differences.
- Real config operations.
load_merge_candidateandload_replace_candidatemodel the device’s actual capability for staged config changes.compare_configshows you the diff before you commit.commit_configapplies it.rollbackundoes it. - Getters return structured data.
get_interfaces()returns a dict of dicts. No parsing. No regex. No pandering to vendor CLI changes between releases.
Weaknesses:
- Limited surface. NAPALM has the getters it has. If you need something outside that surface — vendor-specific commands, niche operational data, anything not in the abstraction — you fall back to either Netmiko or the device’s NETCONF.
- Driver coverage varies. IOS and NX-OS are well-supported. Junos is solid. Some vendors are partially implemented. Always check the getter matrix before assuming a method works on your platform.
- Heavier dependency. NAPALM ships drivers for several platforms and pulls in their respective libraries. Bigger install than Netmiko alone.
The same job in both
Take a common task: across fifty IOS devices, show me which interfaces have CRC errors above zero. Audit-only, no config changes.
In Netmiko:
import csv
from concurrent.futures import ThreadPoolExecutor, as_completed
from netmiko import ConnectHandler
def collect(dev):
with ConnectHandler(**dev) as conn:
conn.enable()
rows = conn.send_command("show interface counters errors", use_textfsm=True)
return dev["host"], rows
with open("crc_audit.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["host", "intf", "crc"])
w.writeheader()
with ThreadPoolExecutor(max_workers=20) as pool:
for fut in as_completed(pool.submit(collect, d) for d in devices):
host, rows = fut.result()
for r in rows:
if int(r.get("crc", 0)) > 0:
w.writerow({"host": host, "intf": r["port"], "crc": r["crc"]})
The same job in NAPALM:
from napalm import get_network_driver
driver = get_network_driver("ios")
with open("crc_audit.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["host", "intf", "crc"])
w.writeheader()
for dev in devices:
with driver(**dev) as d:
counters = d.get_interfaces_counters()
for intf, c in counters.items():
if c["rx_errors"] > 0:
w.writerow({"host": dev["hostname"], "intf": intf, "crc": c["rx_errors"]})
The NAPALM version is shorter and does not require a parser. The Netmiko version handles the same job but goes through TextFSM. Both work. The NAPALM version is more reliable across vendors — get_interfaces_counters() returns the same shape on IOS and NX-OS — but only as long as the getter exists for your platform.
If your fleet were a mix of IOS and NX-OS, NAPALM is dramatically simpler. If your fleet is all IOS, the gap narrows. If your audit is for something not in NAPALM’s getter list, Netmiko wins by default.
Where NAPALM clearly wins
Multi-vendor config compare. “Tell me whether the running config of every device matches the intended config.” NAPALM’s compare_config() does this with one line per device. Doing it with Netmiko means pulling running-config, doing your own diff, and worrying about every vendor’s “show running-config” quirks.
Staged config changes with rollback. NAPALM’s load-candidate / compare / commit / discard / rollback flow models the way IOS-XR, EOS, and modern NX-OS actually want to be configured. With Netmiko you implement this yourself, and you implement it badly the first time.
Standard operational state. get_facts(), get_arp_table(), get_mac_address_table(), get_lldp_neighbors(), get_bgp_neighbors(). These return consistent structures across drivers. For dashboards, CMDB syncs, and inventory enrichment, this saves a lot of parsing work.
A team standardising on a multi-vendor pattern. The same code works against everything, and engineers do not have to learn each vendor’s CLI nuances to read the automation.
Where Netmiko clearly wins
Anything outside NAPALM’s getter list. If the audit is “show me the configured ACLs on every box”, NAPALM might or might not have a getter for that depending on driver and version. Netmiko sends show access-lists and you parse the result.
Vendor-specific commands. Anything Cisco-specific, Fortinet-specific, or platform-specific. NAPALM has cli() as an escape hatch but at that point you are using Netmiko underneath without the syntactic benefits.
Light dependencies. Small scripts, cron jobs, automation that needs to ship into a constrained environment. Netmiko alone is small. NAPALM brings more along.
Fast iteration during troubleshooting. When you are figuring out what command actually returns the data you need, Netmiko’s “send command, see output” loop is quicker. NAPALM is the destination once you know what you want.
Exotic platforms. Anything NAPALM does not have a driver for — and there are a lot of network devices NAPALM does not have a driver for. Netmiko’s platform list is much longer.
The hybrid pattern
In practice, most production tooling I have built ends up using both. NAPALM for config operations and the standard getters; Netmiko for everything else. The pattern that has stuck:
from napalm import get_network_driver
from netmiko import ConnectHandler
class Device:
def __init__(self, host, username, password, vendor_driver, netmiko_type):
self.host = host
self.username = username
self.password = password
self.vendor_driver = vendor_driver
self.netmiko_type = netmiko_type
def napalm(self):
driver = get_network_driver(self.vendor_driver)
return driver(hostname=self.host, username=self.username,
password=self.password)
def netmiko(self):
return ConnectHandler(device_type=self.netmiko_type, host=self.host,
username=self.username, password=self.password)
# Use NAPALM for what it is good at:
with d.napalm() as napalm:
facts = napalm.get_facts()
interfaces = napalm.get_interfaces()
# Drop into Netmiko for the things NAPALM does not cover:
with d.netmiko() as cli:
cli.enable()
raw = cli.send_command("show vendor-specific oddity")
The Device class is doing nothing clever. It is just a holder for the credentials and a factory for two different connection objects. Code that needs structured data and config operations uses .napalm(). Code that needs to send arbitrary CLI uses .netmiko(). Both flow through the same authentication and inventory.
The thing that makes this hybrid work is keeping the two capabilities separate at the call site. Do not write a wrapper that “abstracts both” and ends up reimplementing NAPALM with extra steps. Pick the right tool per task and let each one do its job.
A real production scenario
Take “weekly compliance check”: every router should have NTP configured against a specific server, and any deviation should generate a ticket.
The NAPALM half — pull the running config and compare against intent:
INTENT = """ntp server 10.0.0.10
ntp server 10.0.0.11
"""
with d.napalm() as napalm:
running = napalm.get_config()["running"]
has_ntp = all(line.strip() in running for line in INTENT.strip().splitlines())
That is fine for a binary “configured / not configured” check. It tells you which devices are missing the NTP config.
The Netmiko half — for the devices that are missing config, also check whether NTP is actually working operationally:
with d.netmiko() as cli:
cli.enable()
sync = cli.send_command("show ntp status", use_textfsm=True)
if not sync or sync[0].get("status") != "synchronized":
ticket = open_ticket(d.host, "NTP not synchronized")
The show ntp status data is platform-specific, vendor-flavoured, and not a NAPALM getter. Doing it via Netmiko + TextFSM is the right call. The combination — “intent compliance via NAPALM, operational state via Netmiko” — is the pattern.
When to prefer Scrapli or pure NETCONF
Two adjacent tools worth knowing about even though they are not the focus of this post:
Scrapli is a younger, faster Netmiko replacement. Same idea — SSH driver with platform awareness — but with significantly better performance and a cleaner API. If you are starting a new project from scratch and Netmiko’s slowness has bitten you, Scrapli is worth a look. The mental model is the same; the code is mostly translatable.
NETCONF/RESTCONF via ncclient is the right answer for modern devices that support it. IOS-XR, EOS, and modern NX-OS all expose YANG-modelled config and operational state via NETCONF. The data is structured at the source, no parsing required. The downside is that not every device supports it, the YANG models are a learning curve, and tooling around it is heavier. Worth investing in for greenfield work.
NAPALM itself can use NETCONF as a transport for some drivers, which is part of why its model lines up with operational state schemas. If your roadmap is “we are going to be all-NETCONF in two years”, NAPALM is a sensible bridge.
A cheat sheet
| Task | Netmiko | NAPALM |
|---|---|---|
| Run an arbitrary show command | yes | only via cli() escape hatch |
| Run vendor-specific command | yes | no (without escape hatch) |
| Standard inventory facts (model, OS, serial) | manual parsing | get_facts() |
| Interface state and counters | parse | get_interfaces(), get_interfaces_counters() |
| BGP neighbors | parse | get_bgp_neighbors() |
| ARP / MAC table | parse | get_arp_table(), get_mac_address_table() |
| Send a list of config commands | send_config_set() | load_merge_candidate() |
| Compare current vs intended config | manual diff | compare_config() |
| Atomic commit with rollback | implement yourself | built in |
| Run on Linux servers | yes | no |
| Run on FortiGate | yes | no driver |
| Tiny dependency footprint | yes | no |
The honest summary: if your work is “read state and produce reports” on a single Cisco vendor, Netmiko + TextFSM does the job. If your work is “manage configuration across multiple vendors with rollback”, NAPALM earns its complexity. If your work is both — and it usually becomes both — use both, and keep the call sites clean.
The next post in this series moves from “one device at a time” to running the same task across an inventory at scale, which is where Nornir comes in. Nornir does not replace either of these libraries; it uses them as connection plugins and adds the inventory, parallelism, and result-handling layer on top.