Skip to content
Log in Sign up My account

Netmiko for Network Engineers

Netmiko is a Python library that drives the CLI of network devices over SSH. It handles prompts, paging, enable mode and config mode so you can write scripts instead of expect-style screen scrapers. This article takes you from a first connection to a concurrent, error-tolerant script that collects show output from many devices, parses it into data, and pushes configuration safely.

If you are still deciding between tools, read the NAPALM and Ansible articles too. Netmiko is the right starting point because it works on anything with a CLI, and the CLI is the one interface every device has.

Install and first connection

Use a virtual environment and Python 3.10 or newer.

python3 -m venv .venv
source .venv/bin/activate
pip install netmiko

Netmiko 4.x installs ntc-templates as a dependency, which is what the TextFSM parsing below uses.

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_xe",
    "host": "192.0.2.10",
    "username": "automation",
    "password": "use-an-env-var-not-this",
    "secret": "enable-secret",   # only needed if you use enable mode
}

with ConnectHandler(**device) as conn:
    conn.enable()
    print(conn.find_prompt())
    output = conn.send_command("show version")
    print(output)

The with block disconnects cleanly when it exits. device_type selects the platform driver. Common values are cisco_ios, cisco_xe, cisco_nxos, cisco_xr, cisco_asa and arista_eos. For a platform not on that list, run python -c "from netmiko import ssh_dispatcher; print(sorted(ssh_dispatcher.CLASS_MAPPER_BASE))" to see what your installed version supports.

Warning

Do not hard-code credentials. Read them from environment variables, a secrets manager, or getpass. Anything in a script ends up in Git sooner or later.

import os
device["username"] = os.environ["NET_USER"]
device["password"] = os.environ["NET_PASS"]

Running show commands

send_command() sends one command and returns the output as a string. It waits until the prompt returns, so you do not need terminal length 0; Netmiko disables paging when it connects.

out = conn.send_command("show ip interface brief")

Two parameters matter in practice:

  • read_timeout is how long Netmiko waits for output, in seconds. The default is 10. Slow commands such as show tech-support or a large show ip route need more. In Netmiko 4.x this replaces the old delay_factor tuning.
  • expect_string is a regex for the prompt or text that ends the command. You need it when the command ends on something other than the normal prompt, for example a confirmation question.
out = conn.send_command("show running-config", read_timeout=60)

out = conn.send_command_timing("copy running-config startup-config")

send_command_timing() ignores prompts and waits for output to go quiet. It is the fallback when prompt detection fails, but it is slower and less reliable than send_command().

Parsing output into data

Raw text is fine for a human and useless for logic. Netmiko can hand the output to a parser and return Python data structures instead.

TextFSM with ntc-templates

interfaces = conn.send_command("show ip interface brief", use_textfsm=True)
for intf in interfaces:
    if intf["status"] == "up" and intf["proto"] == "down":
        print(intf["interface"], "is up/down")

With use_textfsm=True you get a list of dictionaries, one per row. The field names come from the template in ntc-templates, so print one result to see the keys before you write code against them. If no template matches the command and platform, Netmiko returns the raw string unchanged, so check the type:

if not isinstance(interfaces, list):
    raise RuntimeError("No TextFSM template matched")

Command abbreviations matter. The template index matches the command text, and Netmiko handles common abbreviations, but typing the full command removes doubt.

Genie

Cisco's pyATS library ships Genie parsers that return nested dictionaries and cover many more commands per platform than ntc-templates.

pip install "pyats[library]"
bgp = conn.send_command("show ip bgp summary", use_genie=True)
neighbors = bgp["vrf"]["default"]["neighbor"]

Genie output is structured by the model Cisco chose, which is usually deeper than TextFSM output. Pick one parser per script and stay with it. TextFSM is lighter to install; Genie wins when you need a command ntc-templates does not cover.

Pushing configuration

send_config_set() enters config mode, sends each line, and exits. It takes a list of commands.

commands = [
    "interface GigabitEthernet1/0/10",
    "description uplink to dist-2",
    "switchport mode trunk",
    "switchport trunk allowed vlan 10,20,30",
]
output = conn.send_config_set(commands)
print(output)
conn.save_config()

save_config() runs write memory (or the platform equivalent). It is not automatic, so a reload will lose anything you forgot to save.

To push a file, use send_config_from_file("changes.cfg").

send_config_set() does not tell you whether the device accepted each line. The output contains the device's error text, such as % Invalid input detected, and Netmiko will not raise an exception for it. Check the output yourself:

ERRORS = ("% Invalid", "% Incomplete", "% Ambiguous", "% Access denied")

def push(conn, commands):
    output = conn.send_config_set(commands)
    bad = [line for line in output.splitlines() if line.startswith(ERRORS)]
    if bad:
        raise RuntimeError("Config errors: " + "; ".join(bad))
    return output

Netmiko 4.x also offers error_pattern on send_config_set(), which raises ConfigInvalidException when a line of output matches. Either approach works; the point is to not trust silence.

Tip

Capture show running-config before and after a change and save both. A diff of the two is your change record, and the "before" copy is your manual rollback.

Handling errors

Connections fail in a few well-known ways, and Netmiko has an exception for each. Import them from netmiko:

from netmiko import (
    ConnectHandler,
    NetmikoTimeoutException,
    NetmikoAuthenticationException,
)
from paramiko.ssh_exception import SSHException

def collect(device):
    try:
        with ConnectHandler(**device) as conn:
            return device["host"], conn.send_command("show version", use_textfsm=True)
    except NetmikoTimeoutException:
        return device["host"], "ERROR: timeout (unreachable or SSH not responding)"
    except NetmikoAuthenticationException:
        return device["host"], "ERROR: authentication failed"
    except SSHException as exc:
        return device["host"], f"ERROR: SSH problem: {exc}"

Catch the specific cases so your report says why a device failed. Authentication failures deserve special care at scale: a bad password pushed to 200 devices can lock the automation account everywhere at once. Stop the run when you see the first authentication failure instead of letting every thread retry.

Many devices at once

Doing 200 devices one at a time is slow because most of the time is spent waiting on the network. Threads fit this job well.

from concurrent.futures import ThreadPoolExecutor, as_completed
import os, yaml

with open("inventory.yaml") as fh:
    hosts = yaml.safe_load(fh)["hosts"]

def build(host):
    return {
        "device_type": "cisco_xe",
        "host": host,
        "username": os.environ["NET_USER"],
        "password": os.environ["NET_PASS"],
        "conn_timeout": 20,
    }

results = {}
with ThreadPoolExecutor(max_workers=20) as pool:
    futures = [pool.submit(collect, build(h)) for h in hosts]
    for fut in as_completed(futures):
        host, data = fut.result()
        results[host] = data

failed = {h: d for h, d in results.items() if isinstance(d, str)}
print(f"{len(results) - len(failed)} ok, {len(failed)} failed")
for h, msg in failed.items():
    print(h, msg)
graph LR
    INV["inventory.yaml"] --> POOL["ThreadPoolExecutor<br/>20 workers"]
    POOL --> D1["Device 1"]
    POOL --> D2["Device 2"]
    POOL --> D3["Device N"]
    D1 --> RES["results dict"]
    D2 --> RES
    D3 --> RES
    RES --> REP["Report and failures"]

Pick max_workers deliberately. Each worker is an SSH session that uses CPU on the device's control plane, and AAA servers have limits. Start around 10 to 20, watch the TACACS+ or RADIUS server, and raise it if both sides are comfortable. For configuration changes, roll out in batches (a pilot device, then a small group, then the rest) and stop if the failure count passes a threshold.

Each thread builds its own connection. Never share one ConnectHandler object between threads.

Practical habits

  • Keep the device list in a file (YAML or CSV), not in code. Better, generate it from your source of truth.
  • Log with the logging module, and set session_log="netmiko.log" in the device dictionary when debugging a stubborn prompt problem. Session logs contain everything sent, including secrets typed at prompts, so do not leave them enabled in production.
  • Set fast_cli=True only after your script works. It trims internal delays, and on slow devices it can cut off output.
  • Use conn.send_command("show clock") as a cheap sanity test when you suspect a session is hung.
  • Prefer send_command() for reads and send_config_set() for writes. Do not send config lines through send_command().
  • On devices that need enable, call conn.enable() after connecting and supply secret in the device dictionary.

Tip

Netmiko is a good fit for one-off audits, ad-hoc changes and getting data out of platforms that have no API. When you need diffs, atomic commits and rollback across vendors, look at NAPALM. When you need inventory, group variables and a repeatable change process for a team, look at Ansible.

References