Network Automation Tools Compared: Ansible, Netmiko, NAPALM and pyATS

Network automation tools compared on one real task: Netmiko, NAPALM, pyATS, Nornir and Ansible, with code, actual parser output and what to learn first.
15min
Read Time
An open laptop on a wooden bench joined by one grey cable to a small dark network switch with green status lights, under a light blue fade from the bottom-left corner.
Network automation tools compared on one real task: Netmiko, NAPALM, pyATS, Nornir and Ansible, with code, actual parser output and what to learn first.

The network automation tools you’ll meet first each do a different job. Netmiko sends CLI commands over SSH. NAPALM gives you the same getters and config replace across vendors. pyATS parses, tests and diffs network state. Ansible pushes config from YAML without you writing Python. And Nornir runs Python tasks on many devices at once.

So don’t ask which one is best. Ask which job you need done.

To show the difference, we ran one show ip interface brief through all five. They didn’t even agree on how many interfaces had a problem. That run is the middle of this post, with the code and the real output.

ToolIts one jobYou writeVersion (2026-10-22)
NetmikoSend commands over SSH, get text or parsed rows backPython4.8.0
NAPALMSame getters and config workflow on every supported vendorPython5.2.0
pyATS (with Genie)Parse, snapshot, test and diff device statePython + YAML testbed26.9
AnsibleDeclare config and push it to a fleetYAML playbooksansible-core 2.21.5
NornirRun Python tasks on many devices in parallelPython + YAML inventory3.6.0

What are network automation tools, and what do they actually do?

Network automation tools are the software that configures, checks and tests network devices for you, instead of you typing into one CLI session at a time.

So what is network automation, exactly? Cisco defines network automation as automating the “configuring, managing, testing, deploying, and operating” of physical and virtual devices.

That definition covers two very different shelves.

On one shelf are network automation platforms: commercial network automation software like Cisco Crosswork, NSO or a config manager with a GUI and a licence. They’re bought by teams.

On the other shelf are the open-source libraries and frameworks you install with pip on your own laptop. That’s the shelf this post covers, because it’s the one you can learn this weekend for free and the one the CCNA Automation exam expects you to recognise.

Network automation tools compared: which job does each one do?

Here’s the side-by-side. Versions, licences and Python support come from PyPI on 2026-10-22. The install sizes are ours: we installed each tool into its own clean virtual environment on Python 3.14.4 and measured it.

NetmikoNAPALMpyATS + GenieAnsibleNornir
Talks to devices bySSH / Telnet CLIPer-vendor driver (IOS driver uses Netmiko)SSH CLI via its own connection librarySSH via network_cli, or APIsWhatever plugin you load (Netmiko, NAPALM, …)
Gives you backRaw text, or a list of dicts with TextFSMNormalised dicts with fixed keysNested dicts keyed by objectTask results in YAML/JSONA result object per host
Platforms177 platform names7 drivers: ios, iosxr, iosxr_netconf, nxos, nxos_ssh, eos, junosGenie lists 6,440 command strings, 4,420 with an IOS XE parserCollections per vendor (cisco.ios 11.6.0)Inherits from its plugins
LicenceMITApache-2.0Apache 2.0GPL-3.0-or-laterApache-2.0
Python needed3.10+3.10+3.10-3.14 wheels3.12+ on the control node3.10+
Runs on Windows?Yes, with pipNot listed (Linux, macOS)No wheels, use WSLControl node needs WSLCore does, with pip
Install size we measured38 MB, 17 packages78 MB, 42 packages395 MB, 98 packages30 MB core + 22 MB of collections79 MB, 48 packages (with both plugins)

Two numbers in that table surprise people.

pyATS is ten times the size of Netmiko. It isn’t one library, it’s a whole test system (pyATS stands for Python Automation Test System), so that’s fair. Just don’t install it on a 20 GB lab VM and wonder where the disk went.

And NAPALM pulls in Netmiko. Its IOS driver imports Netmiko’s ConnectHandler, so on Cisco IOS, NAPALM is a normalising layer sitting on top of the same SSH session Netmiko would open.

Sketch of a left-to-right chain: a sheet of code points to a light blue box labelled NAPALM, which points to a green box labelled NETMIKO, and a thick turquoise cable runs from NETMIKO into a network switch.

Your script calls NAPALM, and on IOS NAPALM opens the connection through Netmiko. Installing NAPALM 5.2.0 pulled in Netmiko as one of its 42 packages.

What happens when you run the same check through every tool?

Here’s the task. You’ve got three access switches and you want every interface that isn’t up/up. Simple, right?

We built the switch output below in the exact column layout of Cisco’s Catalyst 9300 command reference, then fed it to each tool’s real parser on 2026-10-22. No live switch was involved. The parsed output you’ll see is what the tools actually returned. The device versions of each script were compiled and syntax-checked, and the Nornir inventory and pyATS testbed loaded for real, but they weren’t run against hardware.

Interface              IP-Address      OK? Method Status                Protocol
Vlan1                  unassigned      YES NVRAM  administratively down down
Vlan10                 192.0.2.11      YES NVRAM  up                    up
GigabitEthernet1/0/1   unassigned      YES unset  up                    up
GigabitEthernet1/0/2   unassigned      YES unset  up                    up
GigabitEthernet1/0/3   unassigned      YES unset  down                  down
GigabitEthernet1/0/4   unassigned      YES unset  administratively down down
GigabitEthernet1/0/24  unassigned      YES unset  up                    up

Three lines aren’t up/up. Vlan1 and Gi1/0/4 are shut on purpose. Gi1/0/3 is a real problem: it’s enabled and it’s down.

Keep that in mind. One tool is going to notice the difference.

Netmiko: how do you send a command and get data back?

Netmiko is the shortest path from “I type this in the CLI” to “Python types it for me”. You open a session, send the command, and with use_textfsm=True you get rows instead of a wall of text.

import os
from netmiko import ConnectHandler

switches = ["192.0.2.11", "192.0.2.12", "192.0.2.13"]

for host in switches:
    with ConnectHandler(device_type="cisco_ios", host=host,
                        username=os.environ["NET_USER"],
                        password=os.environ["NET_PASS"]) as conn:
        rows = conn.send_command("show ip interface brief", use_textfsm=True)
    for r in rows:
        if r["status"] != "up" or r["proto"] != "up":
            print(host, r["interface"], r["status"], r["proto"])

The parsing comes from the ntc-templates package (version 9.3.0, 979 templates, 143 of them for cisco_ios). Here’s what it actually returned for the first two rows:

[
  {
    "interface": "Vlan1",
    "ip_address": "unassigned",
    "status": "administratively down",
    "proto": "down"
  },
  {
    "interface": "Vlan10",
    "ip_address": "192.0.2.11",
    "status": "up",
    "proto": "up"
  }
]

It’s a list of 7 dicts. Notice the key is proto, not protocol. And the OK? and Method columns are gone.

Result: 3 interfaces flagged (Vlan1, Gi1/0/3, Gi1/0/4).

If you want ten more Netmiko scripts like this one, our Python for network engineers post walks through backups, Jinja2 templates and safe config pushes.

NAPALM: why does it find one problem instead of three?

NAPALM doesn’t hand you the vendor’s columns. It hands you its own model. get_interfaces() returns the same keys on every driver: is_up, is_enabled, description, last_flapped, mtu, speed and mac_address.

import os
from napalm import get_network_driver

switches = ["192.0.2.11", "192.0.2.12", "192.0.2.13"]
driver = get_network_driver("ios")

for host in switches:
    with driver(host, os.environ["NET_USER"], os.environ["NET_PASS"]) as dev:
        for name, i in dev.get_interfaces().items():
            if i["is_enabled"] and not i["is_up"]:
                print(host, name, "enabled but down")

We ran NAPALM 5.2.0’s real IOS getter on the same switch state. Here’s how it scored each port:

Interfaceis_enabledis_up
Vlan1FalseFalse
Vlan10TrueTrue
GigabitEthernet1/0/1TrueTrue
GigabitEthernet1/0/2TrueTrue
GigabitEthernet1/0/3TrueFalse
GigabitEthernet1/0/4FalseFalse
GigabitEthernet1/0/24TrueTrue

Result: 1 interface flagged. Just Gi1/0/3.

That’s not a bug. The driver sets is_enabled to False whenever the status says “administratively”, so a port you shut on purpose stops looking like an outage. Honestly, that’s usually the answer you wanted in the first place.

Two panels of the same six-port switch. In the TEXT panel, turquoise circles mark three ports: one with a down arrow and two with power-off symbols. In the MODEL panel, only the port with the down arrow is circled.

TEXT is what TextFSM and Genie see: status and protocol strings, so Vlan1, Gi1/0/3 and Gi1/0/4 all fail an up/up test. MODEL is NAPALM’s is_enabled and is_up: the two shut ports (power-off symbols) are disabled, so only Gi1/0/3 is enabled but down.

The other half of NAPALM is config. Every driver has the same six methods: load_merge_candidate, load_replace_candidate, compare_config, commit_config, discard_config and rollback. You load a change, read the diff, then commit or throw it away. Same workflow on IOS, NX-OS, EOS or Junos.

pyATS: what is pyATS, and why is the diff the real feature?

pyATS is Cisco’s Python test framework. Cisco says it was “originally developed for internal Cisco engineering use” and still runs millions of tests a month inside the company, according to its pyATS developer page. Genie is the library on top that holds the parsers and the device models. Since pip install "pyats[library]" installs both, people write “pyATS Genie” as one thing.

You describe your devices once in a testbed file:

devices:
  sw1:
    os: iosxe
    type: switch
    connections:
      cli:
        protocol: ssh
        ip: 192.0.2.11
    credentials:
      default:
        username: "%ENV{NET_USER}"
        password: "%ENV{NET_PASS}"

Then the script is short:

from genie.testbed import load

testbed = load("testbed.yaml")

for name, dev in testbed.devices.items():
    dev.connect(log_stdout=False)
    out = dev.parse("show ip interface brief")
    for intf, d in out["interface"].items():
        if d["status"] != "up" or d["protocol"] != "up":
            print(name, intf, d["status"], d["protocol"])
    dev.disconnect()

Genie 26.9 parsed the same output into this:

{
  "Vlan1": {
    "ip_address": "unassigned",
    "interface_is_ok": "YES",
    "method": "NVRAM",
    "status": "administratively down",
    "protocol": "down"
  },
  "Vlan10": {
    "ip_address": "192.0.2.11",
    "interface_is_ok": "YES",
    "method": "NVRAM",
    "status": "up",
    "protocol": "up"
  }
}

Different shape. It’s a dict keyed by interface name, it keeps the OK? and Method columns, and the key is protocol. Copy your Netmiko loop over and it breaks straight away.

Result: 3 interfaces flagged, same as Netmiko.

So why bother with pyATS? Because of what you do next. Take a snapshot before a change window and another after. Genie’s Diff tells you exactly what moved. We parsed the switch with Gi1/0/3 up, then with it down, and ran the diff:

 interface:
  GigabitEthernet1/0/3:
-  protocol: up
+  protocol: down
-  status: up
+  status: down

Six interfaces didn’t change, so they aren’t in the output. On a 48-port switch, or 200 switches, that’s the whole point. You stop reading output and start reading only what changed.

Sketch of two clipboards labelled BEFORE and AFTER with identical rows, except one row on AFTER is turquoise. Arrows from both lead to a magnifying glass that shows only that turquoise row.

Two parses of the same switch, one before and one after Gi1/0/3 went down. Genie’s Diff drops the 6 interfaces that didn’t change and shows only Gi1/0/3’s status and protocol going from up to down.

One catch: pyATS 26.9 ships wheels for Linux and macOS only. On Windows, run it inside WSL or a Linux VM.

Ansible network automation: what does the playbook version look like?

With Ansible you don’t write the loop. You write what you want, and the cisco.ios collection plus ansible.utils do the SSH and the parsing.

- name: Find interfaces that are not up/up
  hosts: switches
  gather_facts: false
  tasks:
    - name: Run and parse show ip interface brief
      ansible.utils.cli_parse:
        command: show ip interface brief
        parser:
          name: ansible.netcommon.ntc_templates
      register: brief

    - name: Report the ones that are not up/up
      ansible.builtin.debug:
        msg: "{{ item.interface }} is {{ item.status }}/{{ item.proto }}"
      loop: "{{ brief.parsed }}"
      when: item.status != 'up' or item.proto != 'up'

And the inventory:

[switches]
sw1 ansible_host=192.0.2.11
sw2 ansible_host=192.0.2.12
sw3 ansible_host=192.0.2.13

[switches:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_user=admin

Run it with ansible-playbook -i hosts.ini ansible_check.yml -k so it asks for the SSH password. We ran the same parse step on our saved output (cli_parse accepts text: instead of command:), and the debug task printed:

"msg": "Vlan1 is administratively down/down"
"msg": "GigabitEthernet1/0/3 is down/down"
"msg": "GigabitEthernet1/0/4 is administratively down/down"

Result: 3 interfaces flagged, and the keys are proto again. That’s because this parser is ntc-templates, the same one Netmiko used. Ansible didn’t parse anything itself. It called the same TextFSM template.

Ansible’s real strength isn’t this read-only check, though. It’s config. Resource modules like cisco.ios.ios_vlans and cisco.ios.ios_interfaces take the state you want and work out the commands. If you want to see how that behaves when you delete something, our Terraform vs Ansible post runs the same VLAN lab through both.

What about Nornir?

Nornir isn’t another way to talk to a switch. It’s a way to run Python tasks on lots of switches at once, with an inventory. The talking is done by plugins, usually Netmiko or NAPALM.

# config.yaml
inventory:
  plugin: SimpleInventory
  options:
    host_file: inventory/hosts.yaml
runner:
  plugin: threaded
  options:
    num_workers: 10
import os
from nornir import InitNornir
from nornir_netmiko.tasks import netmiko_send_command

nr = InitNornir(config_file="config.yaml")
nr.inventory.defaults.username = os.environ["NET_USER"]
nr.inventory.defaults.password = os.environ["NET_PASS"]

results = nr.run(task=netmiko_send_command,
                 command_string="show ip interface brief", use_textfsm=True)
for host, multi in results.items():
    for r in multi[0].result:
        if r["status"] != "up" or r["proto"] != "up":
            print(host, r["interface"], r["status"], r["proto"])

Same proto key, because under the hood it’s Netmiko and TextFSM again. What changed is that all three switches run in parallel. The threaded runner defaults to 20 workers if you don’t set num_workers.

Sketch of a laptop under a tag reading PARALLEL, with three turquoise arrows of equal length fanning out at the same time to three network switches stacked on the right.

Nornir’s threaded runner sends the task to every host at once. Our config.yaml sets num_workers: 10 for three switches. Leave it out and the default is 20.

Think of Nornir as Ansible’s job (inventory plus “run this everywhere”) done in plain Python, so you can use if, loops and your debugger instead of YAML and Jinja2.

One small flag: the nornir-netmiko plugin hasn’t had a release since 2023-12-07 (version 1.0.1). It still installed cleanly next to Netmiko 4.8.0 for us. Worth knowing before you build a team workflow on it.

So what did the five runs teach you?

Same switch. Same question. Here’s the scorecard:

ToolParser behind itData shapeKey for line protocolInterfaces flagged
Netmikontc-templates (TextFSM)list of dictsproto3
Ansiblentc-templates (TextFSM)list of dictsproto3
Nornirntc-templates via Netmikolist per hostproto3
pyATSGeniedict keyed by interfaceprotocol3
NAPALMits own IOS getterbooleansis_up1

Three lessons fall out of that table.

  1. Your code is tied to the parser, not the device. Swap TextFSM for Genie and every r["proto"] becomes a KeyError.
  2. Normalised beats raw when you have mixed vendors. NAPALM’s is_enabled / is_up reads the same on IOS and Junos. TextFSM column names don’t.
  3. Three of the five are the same parser in different clothes. Learn TextFSM once and you’ve learned it for Netmiko, Nornir and Ansible’s cli_parse.
Sketch of one printed table with a small switch clipped to it, and three arrows leading to a stack of light blue cards labelled LIST, a green folder labelled DICT, and two checkboxes, one ticked in turquoise, labelled BOOLEAN.

One show ip interface brief, three shapes. LIST: TextFSM, used by Netmiko, Nornir and Ansible, gives 7 rows with the key proto. DICT: Genie gives one entry per interface with the key protocol. BOOLEAN: NAPALM gives is_enabled and is_up.

Which of these network automation tools should you learn first?

Our recommendation, if you’re working toward CCNA or CCNA Automation:

  1. Netmiko first. It’s the closest thing to the CLI you already know, it installs on Windows with pip, and you can see every command it sends. A week of scripts with it teaches you SSH sessions, prompts and parsing.
  2. Ansible second. It’s named in the 200-901 blueprint, you’ll be asked to read playbooks, and it’s what most network teams already run for config changes.
  3. pyATS third. It’s also named in the blueprint, and the snapshot-and-diff habit will make you better at change windows straight away.
  4. NAPALM or Nornir when you need them. NAPALM when you’ve got more than one vendor or want config replace with rollback. Nornir when your Python scripts start crawling because they run one device at a time.

Your situation might flip that order. If you’re on a team that already runs Ansible, start there. If you’re in a test or QA role at a vendor or service provider, pyATS might be day one.

Ansible vs Python for network automation: do you have to pick one?

No. Ansible is written in Python, and its network parsing runs on the same libraries you’d call from a script. The real choice is how you want to describe the work.

Pick Ansible for config you want to keep in a known state across a fleet: VLANs, ACLs, NTP, users. You declare it, the resource module works out the commands, and the next run changes nothing if nothing drifted.

Pick Python (Netmiko, NAPALM, pyATS, Nornir) for logic: checks, reports, “if this then that”, calling an API and a switch in the same breath. Network automation Python scripts are just easier to debug when the job is mostly decisions.

Most people end up with both. A playbook makes the change, a Python check proves it worked.

Where do network automation tools show up on the CCNA Automation exam?

DevNet Associate became CCNA Automation (200-901 CCNAAUTO) on 2026-02-03. Cisco’s own rebrand session slides say the associate exam was “only rebranded, no updates to exam content”, so the v1.1 topics still apply. It’s a 120-minute exam, per Cisco’s CCNAAUTO course outline.

Tools are named in domain 5.0, Infrastructure and Automation, which is 20% of the 200-901 v1.1 exam topics:

ObjectiveWhat it saysTools in this post
5.3“Describe the use and roles of network simulation and test tools (such as Cisco Modeling Labs and pyATS)”pyATS
5.6“Describe the capabilities of automation tools such as Ansible, Terraform, and Cisco NSO”Ansible
5.8“Identify the workflow being automated by an Ansible playbook”Ansible
3.1“Construct a Python script that uses a Cisco SDK given SDK documentation”Python skills from Netmiko and pyATS help here

Here’s the honest bit. Netmiko, NAPALM and Nornir aren’t named in the blueprint. You learn them because they’re what you’ll use at work, and because writing Python against real devices makes the exam’s API and parsing objectives far easier. For the exam itself, you need to know what pyATS and Ansible are for and be able to read a playbook.

Our CCNA 200-901 skills guide covers the rest of the blueprint, and Terraform’s place in 5.6 is in the Terraform vs Ansible comparison.

What mistakes do people make with these tools?

These all trip people up in the first month.

  • Hard-coding key names, then switching parser. You saw it above: proto vs protocol. Wrap parsing in one function so a parser swap is a one-line fix.
  • Thinking NAPALM avoids screen-scraping on IOS. It doesn’t. Its IOS driver uses Netmiko and regex. What it gives you is a stable output model, not a different transport.
  • Installing pyATS on plain Windows. There’s no Windows wheel. Use WSL, a Linux VM or your lab server.
  • Running pip install ansible-core and expecting Cisco modules. Core doesn’t include them. Add ansible-galaxy collection install cisco.ios, which also pulls ansible.netcommon and ansible.utils. You also need paramiko or ansible-pylibssh for the SSH connection. ansible-core 2.21 wants Python 3.12 or newer on the control node.
  • Counting shut ports as outages. Decide whether “administratively down” is a problem before you alert on it. NAPALM already made that call for you.
  • Putting passwords in the script. Every example here reads them from environment variables. Keep it that way, and use Ansible Vault for playbooks.

Where can you practise without breaking a real network?

You need something with an SSH prompt that answers show ip interface brief. Three options:

  • EVE-NG or GNS3 on a home lab box. Our home lab setup guide gets you there in a weekend.
  • Cisco Modeling Labs. It’s the simulation tool named next to pyATS in objective 5.3. Check the tiers in our CML pricing and limits post.
  • Cisco DevNet Sandbox. Free always-on and reservable labs run by Cisco, with nothing to install.

If you want a guided path, our CCNA Automation workbook has 60 labs built on DevNet Sandbox, including Netmiko scripts for pulling device data, Ansible playbooks that push VLANs and ACLs to IOS XE, and pyATS validation in its study plan. For deeper Ansible, the DevOps workbook goes further. Keep the free Ansible commands cheat sheet and Cisco commands cheat sheet open while you work.

FAQ

What is pyATS?

pyATS is Cisco’s Python Automation Test System, a framework for connecting to network devices, parsing their output into structured data, and testing or comparing that state. Cisco built it for internal engineering and released it publicly. Genie, the library that ships with it, holds the parsers: version 26.9 lists 6,440 command strings, 4,420 of them with an IOS XE parser.

What best describes a pyATS testbed?

A pyATS testbed is the top-level object that holds every device you want to work with: each device’s OS, how to connect to it, its credentials, and optionally the links between interfaces. You usually write it as a YAML file and load it with genie.testbed.load(). Your scripts then refer to devices by name instead of IP.

What are the benefits of pyATS?

The big ones are structured output, vendor-neutral device models, and snapshot comparison. You get Python dicts instead of screen text, you can learn a feature’s state before and after a change, and Diff shows only what moved. It’s also named in the CCNA Automation blueprint (objective 5.3), so time spent on it counts for the exam.

Is Ansible better than Python for network automation?

Neither is better overall. Ansible is better for keeping config in a declared state across many devices without writing code. Python is better for checks, reports and anything with logic. Ansible itself runs on Python, and most teams use both: a playbook to change, a script to verify.

Are network automation tools free?

The ones in this post are. Netmiko is MIT-licensed, NAPALM, Nornir and pyATS are Apache 2.0, and ansible-core is GPL-3.0-or-later, all free to install from PyPI. You pay for commercial network automation platforms such as Red Hat Ansible Automation Platform, Cisco NSO or Crosswork, which add vendor support and management features on top.

What does a network automation engineer use day to day?

Usually Python with Netmiko or NAPALM, Ansible for config changes, Git for version control, and a source of truth such as NetBox for inventory. pyATS shows up wherever testing and change validation matter. Our network automation engineer salary guide maps those skills to pay, and the NetBox Ansible inventory post shows the source-of-truth half. If you’re interviewing, try our Ansible interview questions.

Bottom line

Network automation tools aren’t rivals. They’re layers. Netmiko talks, NAPALM normalises, pyATS tests, Ansible declares, Nornir scales.

Start with Netmiko so you see what’s happening on the wire. Add Ansible and pyATS because the exam and most network teams expect them. Then reach for NAPALM and Nornir when your network or your scripts outgrow the basics.

And whichever you pick, remember the proto vs protocol lesson. The tool’s data model is part of the tool.

Keep Reading

Related Articles

Share Your Valuable Opinions