In the last part, we got Netmiko up and running, connected to a device, ran a show command, and printed the output. That is enough to write some genuinely useful scripts, but Netmiko can do a lot more than just run a single command.
In this part, we will look at what else Netmiko has to offer. We will cover how to run multiple commands, push configuration changes, load config from a file, save the running config, and use enable mode when the device requires it. We will also look at how to parse command output into structured data instead of raw text, which is a huge time-saver when you are trying to actually use the output for something.
By the end of the chapter, you will have a much better feel for what Netmiko can do, and you will be ready to start writing scripts that actually make changes to your network instead of just reading from it. For the examples in this chapter, I will be using three real Cisco IOS devices in my lab.
devices = [
{
"device_type": "cisco_ios",
"host": "192.168.202.101",
"username": "admin",
"password": "admin"
},
{
"device_type": "cisco_ios",
"host": "192.168.202.102",
"username": "admin",
"password": "admin"
},
{
"device_type": "cisco_ios",
"host": "192.168.202.103",
"username": "admin",
"password": "admin"
}
]
Feel free to swap these out for your own devices as we go through the chapter. The IPs, usernames, and passwords will be different for you, but everything else stays the same.
Running Multiple Commands
So far, we have run a single command per device. In real life, you often want to run several commands one after the other, show version, show ip interface brief, show running-config, and so on. The good news is, once you have a connection open, you can send as many commands as you want without reconnecting each time. Reconnecting is slow, so keeping the connection open and running multiple commands is much faster.
The simplest way is to just call send_command() multiple times.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.101",
"username": "admin",
"password": "admin"
}
connection = ConnectHandler(**device)
print(connection.send_command("show version"))
print(connection.send_command("show ip interface brief"))
print(connection.send_command("show clock"))
connection.disconnect()
This connects once, runs three commands, prints each output, and disconnects.
Looping Through a List of Commands
If you have a set of commands you want to run, it is cleaner to put them in a list and loop through them. This makes it easy to add or remove commands later, and keeps the code tidy.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.101",
"username": "admin",
"password": "admin"
}
commands = [
"show version",
"show ip interface brief",
"show clock",
"show running-config | include hostname"
]
connection = ConnectHandler(**device)
for command in commands:
print(f"--- {command} ---")
output = connection.send_command(command)
print(output)
print()
connection.disconnect()
Output
python3 main.py
--- show version ---
Cisco IOS Software [Dublin], Linux Software (X86_64BI_LINUX-ADVENTERPRISEK9-M), Version 17.12.1, RELEASE SOFTWARE (fc5)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2023 by Cisco Systems, Inc.
Compiled Thu 27-Jul-23 22:33 by mcpre
ROM: Bootstrap program is Linux
r1 uptime is 37 minutes
System returned to ROM by unknown reload cause - suspect boot_data[BOOT_COUNT] 0x9, BOOT_COUNT 0, BOOTDATA 19
System image file is "unix:/iol/iol.bin"
Last reload reason: Unknown reason
This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.
A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html
If you require further assistance please contact us by sending email to
export@cisco.com.
Linux Unix (i686) processor with 550115K bytes of memory.
Processor board ID 1
4 Ethernet interfaces
1024K bytes of NVRAM.
Configuration register is 0x0
--- show ip interface brief ---
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.202.101 YES TFTP up up
Ethernet0/1 172.16.1.0 YES manual up up
Ethernet0/2 unassigned YES unset administratively down down
Ethernet0/3 unassigned YES unset administratively down down
Loopback0 10.0.0.1 YES manual up up
--- show clock ---
*22:04:30.478 UTC Thu Jul 9 2026
--- show running-config | include hostname ---
hostname r1
The loop goes through each command, prints a small header so you can tell which output belongs to which command, sends the command, and prints the result.
Storing the Output for Later
Printing the output is fine when you want to see it immediately, but often you want to store it somewhere so you can use it later, save it to a file, compare it against a baseline, or process it further. A dictionary works well for this, use the command as the key and the output as the value.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.101",
"username": "admin",
"password": "admin"
}
commands = [
"show version",
"show ip interface brief",
"show clock"
]
connection = ConnectHandler(**device)
results = {}
for command in commands:
results[command] = connection.send_command(command)
connection.disconnect()
print(results["show clock"])
Output
python3 main.py
*22:06:05.070 UTC Thu Jul 9 2026
Now every command's output is stored in the results dictionary, and you can access any of them by name. This is the kind of structure that becomes really useful when you want to save it to a JSON file, feed it into another function, or generate a report.
Multiple Commands on Multiple Devices
Putting this all together with our list of devices from the intro, here is how you would run multiple commands on multiple devices.
from netmiko import ConnectHandler
devices = [
{"device_type": "cisco_ios", "host": "192.168.202.101", "username": "admin", "password": "admin"},
{"device_type": "cisco_ios", "host": "192.168.202.102", "username": "admin", "password": "admin"},
{"device_type": "cisco_ios", "host": "192.168.202.103", "username": "admin", "password": "admin"}
]
commands = [
"show version | include Version",
"show ip interface brief"
]
for device in devices:
print(f"=== {device['host']} ===")
connection = ConnectHandler(**device)
for command in commands:
print(f"--- {command} ---")
print(connection.send_command(command))
connection.disconnect()
print()
Output
python3 main.py
=== 192.168.202.101 ===
--- show version | include Version ---
Cisco IOS Software [Dublin], Linux Software (X86_64BI_LINUX-ADVENTERPRISEK9-M), Version 17.12.1, RELEASE SOFTWARE (fc5)
--- show ip interface brief ---
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.202.101 YES TFTP up up
Ethernet0/1 172.16.1.0 YES manual up up
Ethernet0/2 unassigned YES unset administratively down down
Ethernet0/3 unassigned YES unset administratively down down
Loopback0 10.0.0.1 YES manual up up
=== 192.168.202.102 ===
--- show version | include Version ---
Cisco IOS Software [Dublin], Linux Software (X86_64BI_LINUX-ADVENTERPRISEK9-M), Version 17.12.1, RELEASE SOFTWARE (fc5)
--- show ip interface brief ---
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.202.102 YES TFTP up up
Ethernet0/1 172.16.1.1 YES manual up up
Ethernet0/2 172.16.1.2 YES manual up up
Ethernet0/3 unassigned YES unset administratively down down
Loopback0 10.0.0.2 YES manual up up
=== 192.168.202.103 ===
--- show version | include Version ---
Cisco IOS Software [Dublin], Linux Software (X86_64BI_LINUX-ADVENTERPRISEK9-M), Version 17.12.1, RELEASE SOFTWARE (fc5)
--- show ip interface brief ---
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.202.103 YES TFTP up up
Ethernet0/1 172.16.1.3 YES manual up up
Ethernet0/2 172.16.1.4 YES manual up up
Ethernet0/3 unassigned YES unset administratively down down
Loopback0 10.0.0.3 YES manual up up
This is a nested loop, the outer loop goes through each device, and the inner loop runs each command on the current device.
Simplifying When Devices Share Most Details
You might have noticed that in the example above, every device has the same device_type, username, and password. Only the host is different. Writing out the same three fields for every device gets repetitive, especially if you have 20 or 30 devices. There is a cleaner way to handle this.
Instead of a list of full dictionaries, you can keep a list of just the IPs and combine them with a base dictionary that holds the common details.
from netmiko import ConnectHandler
base_device = {
"device_type": "cisco_ios",
"username": "admin",
"password": "admin"
}
hosts = [
"192.168.202.101",
"192.168.202.102",
"192.168.202.103"
]
commands = [
"show version | include Version",
"show ip interface brief"
]
for host in hosts:
device = {**base_device, "host": host}
print(f"=== {host} ===")
connection = ConnectHandler(**device)
for command in commands:
print(f"--- {command} ---")
print(connection.send_command(command))
connection.disconnect()
print()
The interesting line here is this one.
device = {**base_device, "host": host}
This uses dictionary unpacking (similar to what we saw with ** in Chapter 11) to build a new dictionary. It takes every key-value pair from base_device and adds a host key with the current IP. The result is a full device dictionary with all four fields, without having to type the common ones every time.
Sending Configuration Commands
Up to this point, we have only used send_command(), which is designed for show commands and other read-only operations. If you try to use it to send a configuration command like interface Ethernet0/1, it will not work the way you expect, because send_command() does not enter configuration mode on the device. It just sits at the regular prompt and tries to execute whatever you give it.
For configuration changes, Netmiko gives you a completely separate method called send_config_set(). This one is designed for config mode. It takes a list of config commands, enters configuration mode on the device for you, sends the commands one by one, exits configuration mode, and returns the output of the whole thing.
Let's look at a simple example. We will add a description to an interface on one device.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.101",
"username": "admin",
"password": "admin"
}
config_commands = [
"interface Ethernet0/1",
"description Connected to Core Switch"
]
connection = ConnectHandler(**device)
output = connection.send_config_set(config_commands)
print(output)
connection.disconnect()
Output
python3 main.py
configure terminal
Enter configuration commands, one per line. End with CNTL/Z.
r1(config)#interface Ethernet0/1
r1(config-if)#description Connected to Core Switch
r1(config-if)#end
r1#
r1#show interfaces description | incl 0/1
Et0/1 up up Connected to Core Switch
Look at the output. Netmiko automatically ran configure terminal to enter config mode, then sent both of our commands, then ran end to exit config mode. We did not have to include any of that in our list, Netmiko manages the mode switching for us. All we had to give it were the actual commands we cared about.
The commands are always passed in as a list, even if you only have one. This is different from send_command(), which takes a single string.
Another Example
Let's do something a bit more useful. We will create a couple of VLANs on the switch.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.106",
"username": "admin",
"password": "admin"
}
config_commands = [
"vlan 100",
"name USERS",
"vlan 200",
"name SERVERS",
"vlan 300",
"name MGMT"
]
connection = ConnectHandler(**device)
output = connection.send_config_set(config_commands)
print(output)
connection.disconnect()
Output
python3 main.py
configure terminal
Enter configuration commands, one per line. End with CNTL/Z.
sw1(config)#vlan 100
sw1(config-vlan)#name USERS
sw1(config-vlan)#vlan 200
sw1(config-vlan)#name SERVERS
sw1(config-vlan)#vlan 300
sw1(config-vlan)#name MGMT
sw1(config-vlan)#end
sw1#
sw1#show vlan
VLAN Name Status Ports
---- -------------------------------- --------- -------------------------------
1 default active Et0/1, Et0/2, Et0/3
100 USERS active
200 SERVERS active
300 MGMT active
This creates three VLANs (100, 200, 300) and gives each one a name. Netmiko sends them one after the other, all inside a single configuration session.
Loading Config Commands from a File
Hardcoding config commands inside a Python list is fine for small changes, but for anything larger, it is cleaner to keep the config in a separate file and load it from there. Netmiko has a method for this called send_config_from_file().
Say you have a file called vlans.txt in the same folder as your script, with the following contents.
vlan 100
name USERS
vlan 200
name SERVERS
vlan 300
name MGMT
You can push that file straight to the device with a single line.
from netmiko import ConnectHandler
device = {
"device_type": "cisco_ios",
"host": "192.168.202.106",
"username": "admin",
"password": "admin"
}
connection = ConnectHandler(**device)
output = connection.send_config_from_file("vlans.txt")
print(output)
connection.disconnect()
Netmiko reads the file, sends each line as a config command, and returns the output. This is really useful when you have a standard set of configuration that you want to apply to many devices (hundreds or thousands), you write the config once, save it to a file, and then just point Netmiko at it.


