Yesterday you drove a router with Paramiko and felt every bump: manual time.sleep() calls, draining banners, guessing when output was done. Today that all disappears. Netmiko wraps Paramiko specifically for network devices — it knows about prompts, paging, configuration mode, and dozens of vendor platforms. For day-to-day SSH automation, Netmiko is the tool most network engineers actually reach for first.
If you only learn one library from this whole week, make it this one.
Install and Connect
pip install netmiko
A connection is a single dictionary describing the device, passed to ConnectHandler:
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios", # this is the magic field
"host": "192.168.1.1",
"username": "admin",
"password": "cisco123",
"secret": "enablepass", # enable password, if needed
}
conn = ConnectHandler(**device)
print(conn.find_prompt()) # e.g. 'R1>'
conn.disconnect()
The device_type is what makes Netmiko multi-vendor. Change it to arista_eos, juniper_junos, cisco_nxos, cisco_xr, hp_procurve, and Netmiko adjusts its prompt patterns and paging commands automatically. Same code, different platform.
send_command: Read Operations Done Right
This single method replaces all of yesterday’s sleep-and-recv dance. Netmiko sends the command, waits for the prompt to return, strips the echoed command, and hands you clean output:
conn = ConnectHandler(**device)
output = conn.send_command("show ip interface brief")
print(output)
conn.disconnect()
No terminal length 0 — Netmiko disables paging for you. No timing guesswork — it watches for the prompt. This is the payoff for understanding the raw layer yesterday.
The Best Part: send_command with use_textfsm
Add one argument and Netmiko parses the output into structured data using the ntc-templates library (we devote a whole post to it later this week). Instead of a wall of text, you get a list of dictionaries:
data = conn.send_command("show ip interface brief", use_textfsm=True)
# data is now a list of dicts:
# [{'interface': 'GigabitEthernet0/0', 'ipaddr': '192.168.1.1',
# 'status': 'up', 'proto': 'up'}, ...]
for row in data:
if row["status"] != "up":
print(f"{row['interface']} is {row['status']}")
This is enormous. You go straight from a CLI command to data you can filter, count, and compare — no regex required. (Install the templates with pip install ntc-templates; Netmiko uses them automatically when use_textfsm=True.)
Making Configuration Changes: send_config_set
Reading is half the job. To push configuration, hand Netmiko a list of commands and it enters config mode, sends them in order, and exits — all automatically:
commands = [
"interface Loopback100",
"description Created by Netmiko",
"ip address 10.100.100.1 255.255.255.255",
]
output = conn.send_config_set(commands)
print(output)
# Don't forget to save (IOS):
print(conn.save_config()) # issues 'write memory'
Netmiko handles the configure terminal and end wrapping for you. You just describe what to configure. And save_config() knows the right save command per platform — write memory on IOS, commit on Junos, and so on.
enable() and Privileged Mode
If you land in user EXEC mode (R1>), call enable() to get to privileged mode (R1#) using the secret from your device dict:
conn = ConnectHandler(**device)
if not conn.check_enable_mode():
conn.enable()
conn.send_config_set(["ntp server 10.0.0.1"])
Cisco Context: Push One Change to a Fleet
The real win is doing the same thing to many devices. Loop a list, use with so each session closes cleanly, and keep going even if one device fails:
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException
inventory = [
{"device_type": "cisco_ios", "host": "192.168.1.1",
"username": "admin", "password": "cisco123"},
{"device_type": "cisco_ios", "host": "192.168.1.2",
"username": "admin", "password": "cisco123"},
]
config = ["ntp server 10.0.0.1", "logging host 10.0.0.50"]
for dev in inventory:
host = dev["host"]
try:
with ConnectHandler(**dev) as conn:
conn.send_config_set(config)
conn.save_config()
print(f"{host}: applied and saved")
except NetmikoAuthenticationException:
print(f"{host}: AUTH FAILED")
except NetmikoTimeoutException:
print(f"{host}: UNREACHABLE")
That is a production-shaped script: idempotent-ish config, per-device error isolation, automatic cleanup. Swap the inventory for a YAML file (Monday’s topic) and you have a tool.
send_command Timing Knobs
Occasionally a command runs long (a big show tech) or a device is slow. Two arguments handle it: read_timeout raises the ceiling on how long to wait for the prompt, and expect_string lets you wait for a custom prompt (useful right after a hostname change). You rarely need them, but knowing they exist saves an afternoon.
Exercises
- Warm-up. Connect to a device and print its prompt with
find_prompt(), then disconnect cleanly using awithblock. - Read. Run
show versionand print just the line containing the software version (combine with your regex/string skills). - Structured read. Use
send_command(..., use_textfsm=True)onshow ip interface briefand print only interfaces whose protocol isdown. - Config push. Add a loopback interface with a description and a /32 address using
send_config_set, then save the config. - Challenge. Write
audit_ntp(inventory, expected_server)that connects to each device, runsshow running-config | include ntp server, and reports for each device whetherexpected_serveris configured — isolating failures so one dead device does not stop the audit.
Answers
Show answers
1. Warm-up
from netmiko import ConnectHandler
device = {"device_type": "cisco_ios", "host": "192.168.1.1",
"username": "admin", "password": "cisco123"}
with ConnectHandler(**device) as conn:
print(conn.find_prompt())
2. Read the version line
with ConnectHandler(**device) as conn:
out = conn.send_command("show version")
for line in out.splitlines():
if "Version" in line:
print(line.strip())
break
3. Structured read
with ConnectHandler(**device) as conn:
rows = conn.send_command("show ip interface brief", use_textfsm=True)
for r in rows:
if r["proto"] == "down":
print(f"{r['interface']} protocol down (status {r['status']})")
4. Config push
cmds = ["interface Loopback123",
"description lab loopback",
"ip address 10.123.123.1 255.255.255.255"]
with ConnectHandler(**device) as conn:
print(conn.send_config_set(cmds))
print(conn.save_config())
5. Challenge
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException
def audit_ntp(inventory, expected_server):
for dev in inventory:
host = dev["host"]
try:
with ConnectHandler(**dev) as conn:
out = conn.send_command(
f"show running-config | include ntp server")
ok = expected_server in out
print(f"{host}: {'OK' if ok else 'MISSING'} "
f"({expected_server})")
except NetmikoAuthenticationException:
print(f"{host}: AUTH FAILED")
except NetmikoTimeoutException:
print(f"{host}: UNREACHABLE")
Same error-isolation pattern as the fleet push: wrap each device, catch the two common Netmiko exceptions, and keep going. This is the backbone of every multi-device script you will write.
Previously: SSH Automation with Paramiko. Coming tomorrow — NAPALM: one consistent API across Cisco, Arista, and Juniper, with config diffs and rollback built in.