
In modern network administration, Python knowledge is becoming an indispensable tool, and the ability to work with files is a critically important skill. Saving configurations, processing logs, automating data exchange – all this is impossible without competent interaction with structured and unstructured files. In this article, you will learn how to effectively use the capabilities of the language for working with files in the daily practice of a network specialist. This material is created specifically for those who seek to combine the flexibility of Python with real automation tasks in the infrastructure.
In practical work, in order to fully apply the knowledge gained in the previous sections, it is necessary to have a good understanding of working with files. When interacting with network equipment, as well as in many other cases, one has to deal with different types of files. For example, configuration files can be simple text documents without a complex structure. Processing them is one of the topics of this section. There are also configuration templates, for which a special format is usually used and the use of the Jinja2 template generator for their creation is considered.
Another common type is files containing connection parameters; they often have a structure in the form of YAML, JSON or CSV, and methods for processing them are described in the corresponding section on serialization. There is also a need to work with other Python scripts, where it is important to be able to interact competently with modules. In this section, the main emphasis is placed on interacting with simple text files, such as Cisco device configurations. The main actions, which are explained in detail, include opening, reading, writing and closing files. This provides only the basic information needed to confidently work with files in Python, but for in-depth study, you can always refer to the official documentation.
To start working with a file, you need to open it. The most common way to open files is with the open function:
file = open('file_name.txt', 'r')
In the open() function:
The open() function in Python allows you to open files for further work with them. The first argument is the name or path to the file (it can be either an absolute or relative path), the second is the mode in which the file will be opened. For example, the ‘r’ symbol means opening the file for reading only, and this is the default mode. If you want to not only read, but also change the contents, the ‘r+’ mode is used.
To create or completely overwrite a file, the ‘w’ mode is used. In this case, if the file already exists, its contents are completely erased, and if it does not exist, a new one is created. The ‘w+’ mode works similarly, but with the ability to read. If the goal is to add new data to the end of an existing file without affecting the existing information, the ‘a’ mode is used. For reading and appending at the same time, ‘a+’ is used.
After opening a file using open(), a file object is created to which you can apply the read, write, or close methods, depending on the selected mode. r – read; a – append; w – write.
Python has several methods for reading a file:
read- Reads the contents of a file into a line
readline- reads a file line by line
readlines- reads lines from a file and creates a list of the lines
Let’s see how to read file contents using the example of the file r1.txt:
! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
The read method reads the entire file in one line.
Example of using the read method:
In [1]: f = open('r1.txt') In [2]: f.read() Out[2]: '!\nservice timestamps debug datetime msec localtime show-timezone year\nservice timestamps log datetime msec localtime show-timezone year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain lookup\n!\nip ssh version 2\n!\n' In [3]: f.read() Out[3]: ''
When re-reading the file at line 3, an empty line is displayed. This is because the entire file is read when the read.seek method is called.
A file can be read using the readline method:
In [4]: f = open('r1.txt') In [5]: f.readline() Out[5]: '!\n' In [6]: f.readline() Out[6]: 'service timestamps debug datetime msec localtime show-timezone year\n'
But it’s often easier to iterate over the file object in a loop without using the read… methods.
In [7]: f = open('r1.txt') In [8]: for line in f: ...: print(line) ...: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
readlines
Another useful method is readlines. It reads lines from a file into a list:
In [9]: f = open('r1.txt') In [10]: f.readlines() Out[10]: ['!\n', 'service timestamps debug datetime msec localtime show-timezone year\n', 'service timestamps log datetime msec localtime show-timezone year\n', 'service password-encryption\n', 'service sequence-numbers\n', '!\n', 'no ip domain lookup\n', '!\n', 'ip ssh version 2\n', '!\n']
If you need to get the lines of a file, but without translating the line at the end, you can use the split method and specify the \n character as the separator:
In [11]: f = open('r1.txt') In [12]: f.read().split('\n') Out[12]: ['!', 'service timestamps debug datetime msec localtime show-timezone year', 'service timestamps log datetime msec localtime show-timezone year', 'service password-encryption', 'service sequence-numbers', '!', 'no ip domain lookup', '!', 'ip ssh version 2', '!', '']
Note that the last element of the list is an empty string.
If you use the rstrip method before performing split, the list will be without the empty string at the end:
In [13]: f = open('r1.txt') In [14]: f.read().rstrip().split('\n') Out[14]: ['!', 'service timestamps debug datetime msec localtime show-timezone year', 'service timestamps log datetime msec localtime show-timezone year', 'service password-encryption', 'service sequence-numbers', '!', 'no ip domain lookup', '!', 'ip ssh version 2', '!']
seek
Until now, the file had to be reopened every time to count it again. This happens because after the read methods, the cursor is at the end of the file. And re-reading returns an empty line. To count information from the file again, you need to use the seek method, which moves the cursor to the desired position.
Example of opening a file and reading the contents:
In [15]: f = open('r1.txt') In [16]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
If you call the read method again, an empty string is returned:
In [17]: print(f.read())
But using the seek method, you can go to the beginning of the file (0 means the beginning of the file):
In [18]: f.seek(0)
After the cursor has been moved to the beginning of the file using seek, you can read the contents again:
In [19]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
When recording, it is very important to decide on the file opening mode so as not to accidentally delete it:
w- Open file for writing. If the file exists, its contents are deleted
a- Open file for appending. The data is appended to the end of the file
Both modes create the file if it does not exist.
The following methods are used to write to a file:
write- write a single line to a file
writelines- allows you to pass a list of lines as an argument
write
The write method waits for a string to write.
For example, let’s take a list of strings with the configuration:
In [1]: cfg_lines = ['!', ...: 'service timestamps debug datetime msec localtime show-timezone year', ...: 'service timestamps log datetime msec localtime show-timezone year', ...: 'service password-encryption', ...: 'service sequence-numbers', ...: '!', ...: 'no ip domain lookup', ...: '!', ...: 'ip ssh version 2', ...: '!']
Opening the file r2.txt in write mode:
In [2]: f = open('r2.txt', 'w')
Let’s convert the list of commands into one large string using join:
In [3]: cfg_lines_as_string = '\n'.join(cfg_lines) In [4]: cfg_lines_as_string Out[4]: '!\nservice timestamps debug datetime msec localtime show-timezone year\nservice timestamps log datetime msec localtime show-timezone year\nservice password-encryption\nservice sequence-numbers\n!\nno ip domain lookup\n!\nip ssh version 2\n!'
Writing a line to a file:
In [5]: f.write(cfg_lines_as_string)
Similarly, you can add the line manually:
In [6]: f.write('\nhostname r2')
After you finish working with the file, you must close it:
In [7]: f.close()
Since ipython supports the cat command, you can easily view the contents of a file:
In [8]: cat r2.txt ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 ! hostname r2
writelines
The writelines method expects a list of lines as an argument.
Writing the list of lines cfg_lines to a file:
In [1]: cfg_lines = ['!', ...: 'service timestamps debug datetime msec localtime show-timezone year', ...: 'service timestamps log datetime msec localtime show-timezone year', ...: 'service password-encryption', ...: 'service sequence-numbers', ...: '!', ...: 'no ip domain lookup', ...: '!', ...: 'ip ssh version 2', ...: '!'] In [9]: f = open('r2.txt', 'w') In [10]: f.writelines(cfg_lines) In [11]: f.close() In [12]: cat r2.txt !service timestamps debug datetime msec localtime show-timezone yearservice timestamps log datetime msec localtime show-timezone yearservice password-encryptionservice sequence-numbers!no ip domain lookup!ip ssh version 2!
As a result, all the lines from the list were written to a single line of the file, because there was no \n character at the end of the lines.
There are different ways to add line translation. For example, you can simply process the list in a loop:
In [13]: cfg_lines2 = [] In [14]: for line in cfg_lines: ....: cfg_lines2.append(line + '\n') ....: In [15]: cfg_lines2 Out[15]: ['!\n', 'service timestamps debug datetime msec localtime show-timezone year\n', 'service timestamps log datetime msec localtime show-timezone year\n', 'service password-encryption\n', 'service sequence-numbers\n', '!\n', 'no ip domain lookup\n', '!\n', 'ip ssh version 2\n',
If you write the resulting list back to a file, it will already contain the string translations:
In [18]: f = open('r2.txt', 'w') In [19]: f.writelines(cfg_lines2) In [20]: f.close() In [21]: cat r2.txt ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
In real life, the most common way to close files is with. It is much more convenient to use than to explicitly close a file. However, since the close method is also available in real life, this section discusses how to use it.
After you are done working with a file, you need to close it. In some cases, Python can close the file itself. But it is better not to count on it and close the file explicitly.
close
The close method was found in the file writing section. There it was needed so that the contents of the file were written to disk.
Python has a separate flush method for this. But since in the file writing example, no operations were required, the file could be closed.
Let’s open the file r1.txt:
In [1]: f = open('r1.txt', 'r')
Now you can read the contents:
In [2]: print(f.read()) ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
The file object has a special attribute called closed that allows you to check whether the file is closed or not. If the file is open, it returns False:
In [3]: f.closed Out[3]: False
Now close the file and check closed again:
In [4]: f.close() In [5]: f.closed Out[5]: True
If you try to read the file, an exception will occur:
In [6]: print(f.read()) ------------------------------------------------------------------ ValueError Traceback (most recent call last) <ipython-input-53-2c962247edc5> in <module>() ----> 1 print(f.read()) ValueError: I/O operation on closed file
The with construct is called a context manager. Python has a more convenient way of working with files than the ones we’ve used so far – the with construct:
In [1]: with open('r1.txt', 'r') as f: ....: for line in f: ....: print(line) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
In addition, the with construct ensures that the file is closed automatically.
Notice how the lines of the file are read:
for line in f: print(line)
When you need to work with a file line by line, it is better to use this option.
In the previous output, there were extra blank lines between the lines of the file, because print adds another line break.
To get rid of this, you can use the rstrip method:
In [2]: with open('r1.txt', 'r') as f: ....: for line in f: ....: print(line.rstrip()) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 ! In [3]: f.closed Out[3]: True
And of course, with the with construct you can use not only this string reading option, all the methods considered before also work:
In [4]: with open('r1.txt', 'r') as f: ....: print(f.read()) ....: ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Sometimes you need to work with two files at the same time. For example, you need to write some lines from one file to another.
In this case, you can open two files in a with block like this:
In [5]: with open('r1.txt') as src, open('result.txt', 'w') as dest: ...: for line in src: ...: if line.startswith('service'): ...: dest.write(line) ...: In [6]: cat result.txt service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers
This is equivalent to these two with blocks:
In [7]: with open('r1.txt') as src: ...: with open('result.txt', 'w') as dest: ...: for line in src: ...: if line.startswith('service'): ...: dest.write(line) ...:
This section covers working with files and combines the topics of files, loops, and conditions.
When processing command output or configuration, you will often need to write summary data to a dictionary. It is not always obvious how to process command output and how to generally approach parsing output. This section covers several examples, with increasing difficulty.
In this example, we will analyze the output of the sh ip int br command. From the command output, we need to get the interface name – IP address correspondence. That is, the interface name is the dictionary key, and the IP address is the value. In this case, the correspondence should be done only for those interfaces that have an IP address assigned.
Example of the output of the sh ip int br command (file sh_ip_int_br.txt):
R1#show ip interface brief Interface IP-Address OK? Method Status Protocol FastEthernet0/0 15.0.15.1 YES manual up up FastEthernet0/1 10.0.12.1 YES manual up up FastEthernet0/2 10.0.13.1 YES manual up up FastEthernet0/3 unassigned YES unset up down Loopback0 10.1.1.1 YES manual up up Loopback100 100.0.0.1 YES manual up up
File working_with_dict_example_1.py:
result = {} with open('sh_ip_int_br.txt') as f: for line in f: line_list = line.split() if line_list and line_list[1][0].isdigit(): interface = line_list[0] address = line_list[1] result[interface] = address print(result)
The sh ip int br command displays the output in columns. So, the required fields are on one line. The script processes the output line by line and splits each line using the split method.
The resulting list contains several outputs. Since only interfaces with an IP address configured are needed from the entire output, the first character of the second column is checked: if the first character is a number, specify the address assigned to the interface and this line should be processed.
Since each line has a key and value pair, they are provided in a dictionary: .result[interface] = address
The result of executing the script will be the following dictionary (here it is split into key-value pairs for convenience, in the actual script output the dictionary is displayed on one line):
{'FastEthernet0/0': '15.0.15.1', 'FastEthernet0/1': '10.0.12.1', 'FastEthernet0/2': '10.0.13.1', 'Loopback0': '10.1.1.1', 'Loopback100': '100.0.0.1'}
Very often, the output of commands looks like the key and value are on different lines. And you have to figure out how to process the output to get the desired match.
For example, from the command output, you need to get the interface name – MTU correspondence (file sh_ip_interface.txt):sh ip interface:
Ethernet0/0 is up, line protocol is up Internet address is 192.168.100.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/1 is up, line protocol is up Internet address is 192.168.200.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/2 is up, line protocol is up Internet address is 19.1.1.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ...
The interface name is in the line of type , and the MTU is in the line of type .Ethernet0/0 is up, line protocol is upMTU is 1500 bytes
For example, let’s try to remember the interface every time and print its value when MTU is encountered, along with the MTU value:
In [2]: with open('sh_ip_interface.txt') as f: ...: for line in f: ...: if 'line protocol' in line: ...: interface = line.split()[0] ...: elif 'MTU is' in line: ...: mtu = line.split()[-2] ...: print('{:15}{}'.format(interface, mtu)) ...: Ethernet0/0 1500 Ethernet0/1 1500 Ethernet0/2 1500 Ethernet0/3 1500 Loopback0 1514
The output is organized in such a way that the interface line comes first, and then a few lines later the MTU line. If you remember the interface name every time it is encountered, then by the time you encounter the MTU line, the last interface you remember is the one to which the MTU belongs.
Now, if you want to create a dictionary that matches the interface to the MTU, you just need to write down the value at the time the MTU was found.
The working_with_dict_example_2.py file:
result = {} with open('sh_ip_interface.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] elif 'MTU is' in line: mtu = line.split()[-2] result[interface] = mtu print(result)
The result of executing the script will be the following dictionary (here it is broken into key-value pairs for convenience, in the actual script output the dictionary will be displayed in one line):
{'Ethernet0/0': '1500', 'Ethernet0/1': '1500', 'Ethernet0/2': '1500', 'Ethernet0/3': '1500', 'Loopback0': '1514'}
This technique will be useful quite often, since command output is generally organized in a very similar way.
If you need to get several parameters from the command output, it is very convenient to use a dictionary with a nested dictionary.
For example, from the output of `sh ip interface` you need to get two parameters: IP address and MTU. To start, output information:
Ethernet0/0 is up, line protocol is up Internet address is 192.168.100.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/1 is up, line protocol is up Internet address is 192.168.200.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ... Ethernet0/2 is up, line protocol is up Internet address is 19.1.1.1/24 Broadcast address is 255.255.255.255 Address determined by non-volatile memory MTU is 1500 bytes Helper address is not set ...
In the first step, each value is stored in a variable, and then all three values are printed. The values are printed when the line with MTU is encountered, because it comes last:
In [2]: with open('sh_ip_interface.txt') as f: ...: for line in f: ...: if 'line protocol' in line: ...: interface = line.split()[0] ...: elif 'Internet address' in line: ...: ip_address = line.split()[-1] ...: elif 'MTU' in line: ...: mtu = line.split()[-2] ...: print('{:15}{:17}{}'.format(interface, ip_address, mtu)) ...: Ethernet0/0 192.168.100.1/24 1500 Ethernet0/1 192.168.200.1/24 1500 Ethernet0/2 19.1.1.1/24 1500 Ethernet0/3 192.168.230.1/24 1500 Loopback0 4.4.4.4/32 1514
This uses the same technique as the previous example, but adds another nesting of the dictionary:
result = {} with open('sh_ip_interface.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] result[interface] = {} elif 'Internet address' in line: ip_address = line.split()[-1] result[interface]['ip'] = ip_address elif 'MTU' in line: mtu = line.split()[-2] result[interface]['mtu'] = mtu print(result)
Whenever an interface is encountered, a key with the interface name is created in the ‘result’ dictionary, which corresponds to an empty dictionary. This blank is needed so that at the moment an IP address or MTU is encountered, the parameter can be written to the nested dictionary of the corresponding interface.
The result of executing the script will be the following dictionary (here it is broken into key-value pairs for convenience, in the actual output of the script the dictionary will be displayed on a single line):
{'Ethernet0/0': {'ip': '192.168.100.1/24', 'mtu': '1500'}, 'Ethernet0/1': {'ip': '192.168.200.1/24', 'mtu': '1500'}, 'Ethernet0/2': {'ip': '19.1.1.1/24', 'mtu': '1500'}, 'Ethernet0/3': {'ip': '192.168.230.1/24', 'mtu': '1500'}, 'Loopback0': {'ip': '4.4.4.4/32', 'mtu': '1514'}}
Sometimes, the output will have sections with empty values. For example, in the output case, there may be interfaces that look like this: `sh ip interface`.
Ethernet0/1 is up, line protocol is up Internet protocol processing disabled Ethernet0/2 is administratively down, line protocol is down Internet protocol processing disabled Ethernet0/3 is administratively down, line protocol is down Internet protocol processing disabled
Accordingly, there is no MTU or IP address here.
And if you run the previous script on a file with these interfaces, the result will be like this (output for the file sh_ip_interface2.txt):
{'Ethernet0/0': {'ip': '192.168.100.2/24', 'mtu': '1500'}, 'Ethernet0/1': {}, 'Ethernet0/2': {}, 'Ethernet0/3': {}, 'Loopback0': {'ip': '2.2.2.2/32', 'mtu': '1514'}}
If you need to add interfaces to the dictionary only when an IP address is assigned to the interface, you need to postpone the creation of the key with the interface name to the moment when the line with the IP address is encountered (file working_with_dict_example_4.py):
result = {} with open('sh_ip_interface2.txt') as f: for line in f: if 'line protocol' in line: interface = line.split()[0] elif 'Internet address' in line: ip_address = line.split()[-1] result[interface] = {} result[interface]['ip'] = ip_address elif 'MTU' in line: mtu = line.split()[-2] result[interface]['mtu'] = mtu print(result)
In this case, the result will be the following dictionary:
{'Ethernet0/0': {'ip': '192.168.100.2/24', 'mtu': '1500'}, 'Loopback0': {'ip': '2.2.2.2/32', 'mtu': '1514'}}
The chapter provides basic but fundamental knowledge for working confidently with text files in Python. Through practical examples, it explains how to read, write, and process configuration data from real-world network scenarios, which is essential for effective network automation and analysis.