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_candidate and load_replace_candidate model the device’s actual capability for staged config changes. compare_config shows you the diff before you commit. commit_config applies it. rollback undoes 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

TaskNetmikoNAPALM
Run an arbitrary show commandyesonly via cli() escape hatch
Run vendor-specific commandyesno (without escape hatch)
Standard inventory facts (model, OS, serial)manual parsingget_facts()
Interface state and countersparseget_interfaces(), get_interfaces_counters()
BGP neighborsparseget_bgp_neighbors()
ARP / MAC tableparseget_arp_table(), get_mac_address_table()
Send a list of config commandssend_config_set()load_merge_candidate()
Compare current vs intended configmanual diffcompare_config()
Atomic commit with rollbackimplement yourselfbuilt in
Run on Linux serversyesno
Run on FortiGateyesno driver
Tiny dependency footprintyesno

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.