A Steep Climb or a Smooth Ride? Choosing Between Paramiko and Netmiko

Posted by David Lunel on Thursday, March 5, 2026

What do we choose? Paramiko or Netmiko?

When it comes to automating network devices using Python, two popular libraries often come up: Paramiko and Netmiko. Choosing between these two can be a bit of a dilemma, especially for those new to network automation. Even for experienced network engineers, choosing the right tool can still be a challenge.

Making the decision between Paramiko and Netmiko can feel like a steep climb for beginners. In this post, I’ll show a few use cases and best practices to help you decide which library is the best fit for your needs and how to get started with it.

It is important to remember that both libraries are a means to accomplish a specific use case, not the final solution for an automation project. Both libraries can be used together in a project, depending on the specific requirements of the task at hand.

Steep or Smooth


Prerequisites

Before getting started, make sure you have the following:

  • Basic knowledge of Python and Python 3 installed on your system
  • A basic understanding of how SSH works
  • pip installed, with the ability to install Python packages
  • Access to at least one test device for experimentation (recommended)

The Core Difference

The real difference comes down to the level of abstraction.

Paramiko

  • Low-level SSH library
  • Full control over the connection
  • You handle prompts, timing, parsing, and edge cases

Netmiko

  • Built on top of Paramiko
  • Designed specifically for network devices
  • Handles prompts, entering config mode, and command timing for you

If you are automating common platforms like Cisco, Juniper, or Arista, Netmiko removes a lot of friction.

If you are dealing with an uncommon or quirky device with strange CLI behavior, Paramiko may actually be simpler than forcing a custom Netmiko driver.


The Lab

Let’s set up a simple lab to demonstrate the differences between Paramiko and Netmiko. We will create a folder that can adapt to either library, and we will use the same use case to compare the two approaches.

Basic Setup

Create a simple working directory:

mkdir ntp_update

Create a virtual environment and activate it:

python -m venv .venv
# Unix/Mac
source .venv/bin/activate
# Windows
.venv\Scripts\activate

Install Netmiko and Paramiko using pip:

pip install netmiko paramiko

Devices

Next, we populate the devices.txt file with the IP addresses of the devices we have access to for testing. This file will be used by both the Paramiko and Netmiko scripts to read the list of devices we want to update.

For the sake of this guide, we will use placeholder IP addresses, but in a real scenario, you would replace these with the actual IP addresses of your network devices. If you have non-production devices available, such as lab switches or routers, those would be ideal for testing purposes.

# devices.txt
192.168.1.10
192.168.1.20
192.168.1.30

Services

For our use case, we will be updating the NTP server configuration on our devices. This is a common task that network engineers often need to perform, and it serves as a good example to demonstrate the differences between Paramiko and Netmiko.

Let’s create a file called ntp_servers.txt that contains the new NTP servers we want to configure on our devices. This file will be read by our Netmiko script to update the NTP configuration on each device.

# ntp_servers.txt
ntp1.example.com
ntp2.example.com

Directory Structure

At this point, our directory structure should look like this:

ntp_update/
├── .venv/
├── devices.txt
└── ntp_servers.txt

Use Case: Updating NTP Servers with Netmiko

With this use case, you are tasked with updating the NTP server configuration on a fleet of network devices.

If these devices are from a well-known vendor with predictable CLI behavior, Netmiko is usually the right choice. It understands the device type and manages session handling automatically.

Netmiko Script

We already have our list of devices in devices.txt and our new NTP servers in ntp_servers.txt. The next step is to write a simple Netmiko script that reads these files, connects to each device, and updates the NTP configuration accordingly.

This will be a basic example, and you may need to adjust it based on your specific device types and authentication methods.

Let’s create a file called netmiko_ntp_update.py and add the following code:

from netmiko import ConnectHandler

# Here we read the devices and set them as the variable 'devices'
with open('devices.txt') as f:
    devices = [line.strip() for line in f]

# Here we read the NTP servers and set them as the variable 'ntp_servers'
with open('ntp_servers.txt') as f:
    ntp_servers = [line.strip() for line in f]

# Now we loop through each device, connect, and update the NTP configuration
for device in devices:
    print(f"Connecting to {device}...")
    connection = ConnectHandler(
        device_type='arista_eos',
        host=device,
        username='your_username',
        password='your_password',
    )
    # Enter configuration mode
    connection.enable()
    connection.config_mode()
    # Add new NTP servers
    for ntp in ntp_servers:
        connection.send_command(f'ntp server {ntp}')
    # Save the configuration
    connection.save_config()
    connection.disconnect()
    print(f"Updated NTP servers on {device}")

Execute it with:

python netmiko_ntp_update.py

What’s Happening Here?

Let’s break down the script and see what’s happening at each step:

We import the ConnectHandler class from the Netmiko library, which is used to establish SSH connections to network devices.

from netmiko import ConnectHandler

We read the contents of devices.txt and ntp_servers.txt into lists. Each line is stripped of whitespace and stored in the devices and ntp_servers variables.

with open('devices.txt') as f:
    devices = [line.strip() for line in f]
with open('ntp_servers.txt') as f:
    ntp_servers = [line.strip() for line in f]

We loop through each device in the devices list, and for each device, we establish an SSH connection using the ConnectHandler. We specify the device type, host IP, username, and password.

for device in devices:
    print(f"Connecting to {device}...")
    connection = ConnectHandler(
        device_type='arista_eos', 
        host=device,
        username='your_username',
        password='your_password',
    )

Once connected, we enter enable mode and then configuration mode on the device.

    connection.enable()
    connection.config_mode()

We loop through the list of new NTP servers and send a command to add each one to the device configuration.

    for ntp in ntp_servers:
        connection.send_command(f'ntp server {ntp}')

After updating the configuration, we save it and disconnect from the device.

    connection.save_config()
    connection.disconnect()
    print(f"Updated NTP servers on {device}")

Are we done?

Not exactly. This script is a solid starting point, but it is only the foundation. What we have created is a basic framework that you can extend and refine over time.

To learn more about Python and Netmiko, I recommend experimenting further and adding new features on top of this script, such as:

  • Add error handling to manage connection failures or command errors.
  • Implement logging to keep track of which devices were updated successfully and which ones failed.
  • Add support for different device types by using conditional logic to set the device_type parameter.
  • Use environment variables or a configuration file to store sensitive information like usernames and passwords instead of hardcoding them in the script.
  • Refactor the code to use functions.
  • Refactor the functions into a class to create a more modular and reusable design.

Use Case: Updating NTP Servers with Paramiko via a Jump Host

We are all familiar with environments where direct access to network devices is restricted. You first have to connect to a jump host, and from there SSH into the actual device. Direct access is blocked, and you may not be allowed to install additional software like Python, Netmiko, Paramiko, or any other dependencies on the jump host.

In this use case, you are tasked with updating the NTP server configuration on a fleet of network devices, but you can only access them through a Linux jump host. This is a common scenario in many enterprise environments, and it presents unique challenges that Netmiko may not be well-suited to handle.

Why do we choose Paramiko here?

Paramiko gives you full control over the SSH session. You connect to the jump host, open an interactive shell, and manually initiate the second SSH session to the device.

Netmiko is built around connecting directly to a supported network device. It expects a single, structured CLI session. Once you introduce a Linux jump host and a nested SSH connection, that abstraction becomes a limitation.

Paramiko Script

Since we have already created our devices.txt and ntp_servers.txt files, we can reuse those for our Paramiko script. The main difference is that we will be connecting to the jump host first, and then initiating a second SSH session to the device from there.

Let’s create a file called paramiko_ntp_update.py and add the following code:

import time
import paramiko

# Read devices and NTP servers from files
with open('devices.txt') as f:
    devices = [line.strip() for line in f]
with open('ntp_servers.txt') as f:
    ntp_servers = [line.strip() for line in f]

# Jump host credentials
jump_host = 'jump.host.ip'
jump_user = 'jump_user'
jump_pass = 'jump_password'

# Connect to the jump host
print(f"Connecting to jump host {jump_host}...")
jump_client = paramiko.SSHClient()
jump_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jump_client.connect(jump_host, username=jump_user, password=jump_pass)

# Open an interactive shell on the jump host
jump_shell = jump_client.invoke_shell()

# Loop through each device and connect via the jump host
for device in devices:
    print(f"Connecting to {device} via jump host...")

    # SSH into the device from the jump host
    jump_shell.send(f'ssh your_username@{device}\n')

    # Wait for the shell to prompt for a password
    time.sleep(2)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Send the password for the device
    jump_shell.send('your_password\n')

    # Wait for the shell to log in
    time.sleep(2)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Go into enable mode
    jump_shell.send('enable\n')
    time.sleep(1)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Go into configuration mode
    jump_shell.send('configure terminal\n')
    time.sleep(1)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Now we are connected to the device, we can send commands to update NTP servers
    for ntp in ntp_servers:
        jump_shell.send(f'ntp server {ntp}\n')
        time.sleep(1)
        while not jump_shell.recv_ready():
            time.sleep(1)

    # Save configuration and exit
    jump_shell.send('write memory\n')
    time.sleep(1)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Exit configuration mode
    jump_shell.send('exit\n')
    time.sleep(1)
    while not jump_shell.recv_ready():
        time.sleep(1)

    # Exit the device
    jump_shell.send('exit\n')
    time.sleep(1)
    while not jump_shell.recv_ready():
        time.sleep(1)

    print(f"Updated NTP servers on {device}")

# Close the connection to the jump host
jump_client.close()

Execute it with:

python paramiko_ntp_update.py

What’s Happening Here?

Let’s break down the script and see what’s happening this time:

We import the time library, which is used to add delays in the script to ensure that we wait for the shell to be ready before sending the next command. This is important when working with interactive shells, especially when dealing with nested SSH sessions.

import time

We import the paramiko library, which is used to establish SSH connections and manage SSH sessions.

import paramiko

We read the contents of devices.txt and ntp_servers.txt into lists, just like we did in the Netmiko script.

with open('devices.txt') as f:
    devices = [line.strip() for line in f]
with open('ntp_servers.txt') as f:
    ntp_servers = [line.strip() for line in f]

We define the credentials for the jump host, which we will use to connect before accessing the actual network devices.

jump_host = 'jump.host.ip'
jump_user = 'jump_user'
jump_pass = 'jump_password'

We establish an SSH connection to the jump host using Paramiko’s SSHClient.

print(f"Connecting to jump host {jump_host}...")
jump_client = paramiko.SSHClient()
jump_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
jump_client.connect(jump_host, username=jump_user, password=jump_pass)

We open an interactive shell on the jump host, which allows us to execute commands as if we were logged in directly.

jump_shell = jump_client.invoke_shell()

We loop through each device in the devices list, and for each device, we send a command to SSH into the device from the jump host.

for device in devices:
    print(f"Connecting to {device} via jump host...")
    jump_shell.send(f'ssh your_username@{device}\n')

Several times in the script, we wait for the shell to prompt for input (like a password) before sending the next command. This is done using a loop that checks if the shell is ready to receive data.

    while not jump_shell.recv_ready():
        time.sleep(1)

We wait for the password prompt and send the password to authenticate to the device.

    while not jump_shell.recv_ready():
        pass
    jump_shell.send('your_password\n')

Once we are connected to the device, we can send commands to update the NTP server configuration.

    for ntp in ntp_servers:
        jump_shell.send(f'ntp server {ntp}\n')

After updating the configuration, we save it and exit the device session.

    jump_shell.send('write memory\n')
    jump_shell.send('exit\n')
    
    print(f"Updated NTP servers on {device}")

Finally, we close the connection to the jump host after we have finished updating all devices.

jump_client.close()

Are we done?

You guessed it, not exactly. This script is a solid starting point, but it is only the foundation. What we have created is a basic framework that you can extend and refine over time.

To learn more about Python and Paramiko, I recommend experimenting by adding the same features we mentioned for the Netmiko script and these topics:

  • Consider adding loop protection to handle cases where the SSH session might get stuck waiting for input.
  • Consider making a reusable function to handle the waiting for prompts and sending commands, which can help clean up the code and make it more modular.

Conclusion

For some, it might be a steep climb to get started with network automation, but with the right tools and guidance, it can also be a smooth ride. Don’t worry if it feels overwhelming at first. By reading this blog post, you can identify which tools you can use. The next step is to experiment with them and build your own projects to learn more about network automation.

The best way to learn is by doing, because every use case is different, and you will encounter unique challenges that require creative solutions.

In the next posts that will come, I will write about an overview of tools we can use for network automation. Stay tuned! ⚡