Python Working with Files

Python Working with Files
Table of Contents

Almost every Python script eventually needs to read from or write to a file. You might want to save the output of a command for later, back up device configs to disk, load a list of devices from a text file instead of hardcoding them, or write a log of what your script did. Python makes all of this straightforward once you know how it works.

In this part, we will start with reading text files, cover the proper way to open files using context managers, and then move on to writing and appending. Once we have the basics down, we will build a small config backup script that connects to devices, pulls the running config, and saves each one to its own file on disk.

Reading a Text File

To read a file in Python, you use the built-in open() function. It takes the path to the file as the first argument and returns a file object that you can read from.

Let's say you have a file called devices.txt in the same folder as your script, with the following contents.

192.168.202.101
192.168.202.102
192.168.202.103

Here is the most basic way to read it.

file = open("devices.txt")
contents = file.read()
file.close()

print(contents)

Output

192.168.202.101
192.168.202.102
192.168.202.103

open() opens the file, .read() reads all of its contents into a single string, and .close() closes the file.

read vs readlines

.read() gives you the whole file as one big string, which is fine for small files. But if you want to work with the file line by line, .readlines() is more useful. It returns a list where each item is one line of the file.

file = open("devices.txt")
lines = file.readlines()
file.close()

print(lines)

Output

['192.168.202.101\n', '192.168.202.102\n', '192.168.202.103\n']

Notice the \n at the end of each line. That is the newline character, which is how text files separate one line from the next. Each line in the file ends with \n, and .readlines() includes it in the string. If you want to loop through the lines without the newline characters, you can strip them off with .strip(), which we covered back in Chapter 3.

file = open("devices.txt")
lines = file.readlines()
file.close()

for line in lines:
    clean_line = line.strip()
    print(clean_line)

Output

192.168.202.101
192.168.202.102
192.168.202.103

Looping Through a File Directly

There is actually an even cleaner way to go through a file line by line. The file object itself is iterable, which means you can loop through it directly without calling .readlines().

file = open("devices.txt")

for line in file:
    print(line.strip())

file.close()

Output

192.168.202.101
192.168.202.102
192.168.202.103

This is the most memory-efficient way to read a file, because Python reads one line at a time instead of loading the whole thing into memory.

The Problem with open() and close()

Everything we have done so far works, but there is a problem. If something goes wrong between open() and close(), for example, an exception is raised while reading the file, .close() might never run. That leaves the file open, potentially with unflushed writes, and it is easy to forget to close a file even when nothing goes wrong. Python has a much better way to handle this, which we will look at next.

Context Managers with open()

The recommended way to open files in Python is with a context manager, using the with keyword.

with open("devices.txt") as file:
    contents = file.read()

print(contents)

Output

192.168.202.101
192.168.202.102
192.168.202.103

The with block automatically closes the file for you when the block ends, even if an error happens inside it. You do not have to call .close() yourself. This is the pattern you should use for pretty much every file operation.

You can do everything with a context manager that you could do without one. Here is the line-by-line loop again, using with.

with open("devices.txt") as file:
    for line in file:
        print(line.strip())

Output

192.168.202.101
192.168.202.102
192.168.202.103

What is a Context Manager

A context manager is a Python feature that manages the setup and cleanup of a resource for you. Files are a common example, when you enter the with block, Python opens the file, and when you leave the block, Python closes it. You do not have to think about the cleanup, it just happens.

Files are not the only thing that use context managers. You will see them come up again with database connections, network sockets, and, useful for us, Netmiko connections. Netmiko supports with ConnectHandler(**device) as connection: for exactly the same reason, it automatically disconnects for you when the block ends.

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",
    "host": "192.168.202.101",
    "username": "admin",
    "password": "admin"
}

with ConnectHandler(**device) as connection:
    output = connection.send_command("show ip interface brief")
    print(output)

Notice there is no connection.disconnect() call at the end. When the with block ends, Netmiko automatically closes the SSH session for you. This is cleaner than what we have been doing in earlier chapters, and it means you cannot accidentally forget to disconnect.

From this point forward in the series, we will use the with pattern for both files and Netmiko connections.

Writing to a File

Writing to a file works the same way as reading, you use open() inside a with block, but this time you pass a second argument telling Python you want to write. The mode is the string "w".

with open("output.txt", "w") as file:
    file.write("Hello from Python\n")

After running this, you will have a file called output.txt in the same folder as your script, containing the text Hello from Python.

Two important things to notice here.

First, if the file does not exist, Python creates it. If it does exist, Python overwrites it, wiping out whatever was in it before. Write mode is destructive, be careful with it. If you accidentally open an important file in write mode, its contents are gone.

Second, .write() does not add a newline at the end of your string automatically. If you want each write on its own line, you have to add \n yourself.

Writing Multiple Lines

You can call .write() as many times as you want inside the same with block.

with open("devices.txt", "w") as file:
    file.write("192.168.202.101\n")
    file.write("192.168.202.102\n")
    file.write("192.168.202.103\n")

Or, if you already have a list of things to write, you can loop through them.

devices = ["192.168.202.101", "192.168.202.102", "192.168.202.103"]

with open("devices.txt", "w") as file:
    for device in devices:
        file.write(f"{device}\n")

Both approaches produce the same output. The loop version is cleaner when you have a lot of items or when the items come from another data source.

Using writelines()

There is also a method called .writelines() that takes a list of strings and writes them all at once. It sounds convenient, but it has the same gotcha as .write(), it does not add newlines for you. You have to include them in the strings yourself.

lines = ["192.168.202.101\n", "192.168.202.102\n", "192.168.202.103\n"]

with open("devices.txt", "w") as file:
    file.writelines(lines)

Because of that newline gotcha, most people just use a for loop with .write() instead. It reads more naturally and there is less chance of forgetting the \n.

Writing Netmiko Output to a File

Let's put this together with something more useful. Here is a script that connects to a device, runs a command, and saves the output to a file.

from netmiko import ConnectHandler

device = {
    "device_type": "cisco_ios",
    "host": "192.168.202.101",
    "username": "admin",
    "password": "admin"
}

with ConnectHandler(**device) as connection:
    output = connection.send_command("show ip interface brief")

with open("interface_status.txt", "w") as file:
    file.write(output)

After running this, interface_status.txt contains the exact output you would have seen if you had run show ip interface brief yourself on the device.

Notice that we use two separate with blocks, one for the device connection and one for the file. The device connection is closed as soon as we have the output, and we do not need it open while writing to the file. Keeping the blocks separate makes the intent of the code clearer.

Appending to a File

Sometimes you do not want to overwrite a file, you want to add to what is already there. That is where append mode comes in. You use the same open() function, but this time you pass "a" instead of "w".

with open("log.txt", "a") as file:
    file.write("Script started\n")

If log.txt already exists, this adds Script started at the end of the file. If it does not exist, Python creates it, just like with write mode. The difference is that append mode never wipes the existing contents.

This is really useful for log files, running summaries, or anything where you want to keep adding entries over time without losing what came before.

with open("log.txt", "a") as file:
    file.write("Connected to 192.168.202.101\n")
    file.write("Backup completed\n")
    file.write("Disconnected\n")

Run this a few times and you will see the file grow each time, with new entries added to the bottom. This behaviour is exactly what you want for logs.

Write vs Append at a Glance

  • "w" (write) - creates the file if it does not exist, wipes it if it does, then writes fresh
  • "a" (append) - creates the file if it does not exist, keeps existing contents if it does, then writes at the end

Appending Timestamps to Log Entries

For anything log-like, timestamps make the output much more useful. You can use the datetime module (which we mentioned briefly in Chapter 7) to add one to each entry.

from datetime import datetime

with open("log.txt", "a") as file:
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    file.write(f"[{timestamp}] Backup completed for 192.168.202.101\n")

Contents of log.txt after running

[2026-06-22 22:32:18] Backup completed for 192.168.202.101

datetime.now() gives you the current date and time, and .strftime() formats it into a readable string. The format string "%Y-%m-%d %H:%M:%S" gives you year-month-day and hour:minute:second.

Putting It All Together - A Config Backup Script

Let's tie together everything we have covered in this chapter, along with what we learned about Netmiko, into a proper little config backup script. The script will connect to each device, figure out its hostname, pull the running config, and save it to a file named after the hostname and today's date. We will also add some error handling from the start, so if any device is unreachable or the credentials are wrong, the script skips that device and moves on instead of crashing.

from datetime import datetime
from netmiko import ConnectHandler
from netmiko.exceptions import NetmikoTimeoutException, NetmikoAuthenticationException

devices = [
    {
        "device_type": "cisco_ios",
        "host": "192.168.202.101",
        "username": "admin",
        "password": "admin"
    },
    {
        "device_type": "juniper_junos",
        "host": "192.168.202.102",
        "username": "admin",
        "password": "admin@123"
    },
    {
        "device_type": "arista_eos",
        "host": "192.168.202.103",
        "username": "admin",
        "password": "admin"
    }
]

commands_by_vendor = {
    "cisco_ios": {
        "hostname_command": "show version",
        "config_command": "show running-config"
    },
    "juniper_junos": {
        "hostname_command": "show version",
        "config_command": "show configuration"
    },
    "arista_eos": {
        "hostname_command": "show hostname",
        "config_command": "show running-config"
    }
}

today = datetime.now().strftime("%Y-%m-%d")

for device in devices:
    print(f"Connecting to {device['host']}...")

    vendor_commands = commands_by_vendor[device["device_type"]]

    try:
        with ConnectHandler(**device) as connection:
            hostname_output = connection.send_command(vendor_commands["hostname_command"], use_textfsm=True)
            hostname = hostname_output[0]["hostname"]
            running_config = connection.send_command(vendor_commands["config_command"])

        filename = f"{hostname}_{today}.txt"

        with open(filename, "w") as file:
            file.write(running_config)

        print(f"Saved config for {hostname} to {filename}")

    except NetmikoTimeoutException:
        print(f"Could not reach {device['host']}, skipping")
    except NetmikoAuthenticationException:
        print(f"Authentication failed for {device['host']}, skipping")

print("\nBackup complete")
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to Packetswitch.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.