Reading and writing data¶
A code might require input data and it might also create data for outputting. Therefore it is useful to be able to read and write data from and to a file. This is often referred to as I/O, standing for "Input/Output".
Files can either contain plain ASCII text, i.e., text that is readable if the file is opened with a standard text editor, or information that is in a binary format (generally this is not readable by standard editor unless the format is known).
Plain text files are useful in that they are human readable, although for the same amount of information they will generally be larger in size (i.e., memory taken up on disc) than an equivalent binary file.
Note
When reading and writing it is useful to know your directory structure and be explicit about where you want to save to/read from. This means giving the full path, including directories (and drive letter on Windows), of the file. For example, you might refer to files with:
filename = "C:/My Documents/myfile.txt"
to make sure you are using the file on the C-drive, in the "My Documents"
folder, and with
the name myfile.txt
. Note that the slashes are in the opposite direction (forward slashes) to
the way they are normally shown in Windows. Equivalently you could use backslashes as:
filename = "C:\\My Documents\\myfile.txt"
# or
filename = r"C:\My Documents\myfile.txt"
which both stop Python interpreting backslashes (\
) in a string as an escape character
for the following letter (e.g., \n
in a string means new line).
Another option is to use the pathlib
built-in module to construct Path
objects that can be used instead of strings, e.g.,:
from pathlib import Path
filename = Path("/My Documents/myfile.txt")
On Windows, using pathlib
will often work with or without the drive supplied if the file is
locally stored.
It is useful to save to filenames that do not contain spaces.
Basic reading and writing to file (ASCII text only)¶
The built-in Python function open
provides a way to open files and make them ready for reading their content or writing to them.
When using open
it requires the name of the file to open and the "mode", i.e., whether to open the
file for reading (mode "r"
), writing (mode "w"
), or appending (mode "a"
). It returns a file
object.
Reading¶
Suppose you have a plain text file called mydata.txt
in your current directory containing some
numerical data (this will be used in further examples):
# x y
1 9.8
2 10.3
3 12.4
4 13.2
5 14.7
6 16.1
7 17.2
8 18.7
9 20.1
10 21.3
The first line starts with a #
and is a comment line a giving the "names" of the columns. The two
columns can be read into two lists using the following code:
fp = open("mydata.txt", "r") # open mydata.txt for "r"eading
x = [] # empty list to contain x data
y = [] # empty list to contain y data
for line in fp.readlines(): # loop through each line in the data
if line[0] == "#":
# skip lines that start with a "#"
continue
data = line.split() # split the line on any whitespace
x.append(float(data[0])) # convert string into float
y.append(float(data[1]))
fp.close() # close the file
print(x)
print(y)
[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]
[9.8, 10.3, 12.4, 13.2, 14.7, 16.1, 17.2, 18.7, 20.1, 21.3]
In the above code,
readlines
is a method of the file object. It goes through the file and returns a
list, where each entry is a string containing a line from the file.
One can avoid reading in the entire file at once by simply using for
line in fp:
; this saves time and memory when files are very large.
When reading data in this way you must know what the data file looks like, i.e., you need to know
that comment lines start with a #
and that it contains two columns of numbers.
Note
Here, the file object variable has been named fp
. I have used this as a hangover from writing
C
code where it is often used to mean "file pointer". Any variable name can be given to the
file object.
The open
function can be used as a context
manager. This is basically just a way of
making sure the resource, in this case the open file, is closed after use. Above, the file was
explicitly closed using the close
method, fp.close()
, but you do not need to close the file if
instead you use the
with
statement:
x = [] # empty list to contain x data
y = [] # empty list to contain y data
with open("mydata.txt", "r") as fp:
# indent within the with statement
for line in fp: # loop through each line in the data
if line[0] == "#":
# skip lines that start with a "#"
continue
data = line.split() # split the line on any whitespace
x.append(float(data[0])) # convert string into float
y.append(float(data[1]))
# exited the with statement, but don't need to close fp
Reading binary data can be done by opening the file with the "rb"
mode instead of "r"
and
reading the entire contents using the read
method of the file object. However, converting the read-in data to something that is usable within
Python is trickier as you have to know exactly the layout and memory size for the data stored within
the file. We will not cover this here.
Writing¶
To write to a plain text file you again have to open a file, but this time with the mode set to
write, "w"
. Once the file is open you can then use the
write
method of the file object to add data
to the file. You can only write out string data, so any numbers must be converted to strings.
# create some data
datax = range(10, 21)
datay = [2.3 - 4.5 * x + 5.4 * x ** 2 for x in datax]
filename = "newfile.txt"
fp = open(filename, "w")
for i in range(len(datax)): # loop over the data
fp.write(f"{datax[i]} {datay[i]}\n")
fp.close() # close the file
After this program is run, the contents of file newfile.txt
will look like:
10 497.3
11 606.2
12 725.9
13 856.4
14 997.7
15 1149.8
16 1312.7
17 1486.4
18 1670.9
19 1866.2
20 2072.3
In the output format string f"{datax[i]} {datay[i]}\n"
it separates the two numbers by a
single space and ends with the newline character \n
. If the \n
is not added the numbers would
all be written out on the same line. Values can be separated by multiple spaces, or tabs by using
the tab character \t
.
Warning
If you open a file for writing that already exists it will overwrite the existing file and its contents will be gone. If you want to make sure that the file does not exist before writing you can do something like:
import os # import the built-in os module
filename = "newfile.txt" # name of file to write to
if os.path.isfile(filename):
print("Warning: you are trying to write to an existing file.")
else:
fp = open(filename, "w")
...
Instead of the write
method, the built-in
print
function can also be used to write to
a file. The above code could be replicated with:
filename = "newfile.txt"
fp = open(filename, "w")
for i in range(len(datax)): # loop over the data
print(datax[i], datay[i], file=fp)
fp.close() # close the file
When using print it automatically converts datax[i]
and datay[i]
to their string
representations, automatically adds a space separating them (the separator can be altered using the
sep
keyword argument), and automatically adds a newline character.
Appending¶
Instead of writing to an entirely new file you can append data to an existing file. To do this you
would open the file with the append mode "a"
. If the file does not already exist a new file will
be created. If the file does exist anything you write to it will be added to the end.
Using pathlib
¶
The built-in pathlib
module provides a useful
Path
object for defining file or
directory paths, rather than using strings.
The Path
object has methods for
reading and
writing from and to text
files. For example, to read the mydata.txt
file defined above you could do:
from pathlib import Path
p = Path("mydata.txt")
# read all the contents of the file
contents = p.read_text()
This would read in all the file contents to a string variable, so it would still have to be parsed in some way, e.g.:
x = []
y = []
for line in contents.split("\n"): # loop through each line in the data
if line[0] == "#":
# skip lines that start with a "#"
continue
data = line.split() # split the line on any whitespace
x.append(float(data[0])) # convert string into float
y.append(float(data[1]))
A Path
object can also just be passed to the open
function, or other IO-functions such as those
in NumPy, as if it were a string, e.g.,
from pathlib import Path
p = Path("myfile.txt")
with open(p, "w") as fp:
...
Pickling¶
If your data is purely numerical then writing to a plain text file is a simple way to store it.
However, you may want to save Python objects instead. There is a built-in Python module called
pickle
that allows (most) objects to be saved in
a binary file.
If you define a simple class like:
class MyData:
def __init__(self, x, y, name):
self.x = x
self.y = y
self.name = name
def __str__(self):
return "MyData: '{}'\n x: {}\n y: {}".format(self.name, self.x, self.y)
and then create and instance of that class:
x = [0.7, 0.8, 0.9, 1.0, 1.1]
y = [-10, -9, -8, -7, -6]
mydata = MyData(x, y, "Lab1")
it can be saved in a pickle file using the
dump
method:
import pickle
filename = "mydata.pkl"
fp = open(filename, "wb") # open writing to binary file
pickle.dump(mydata, fp)
fp.close()
This data can then be read back in using the
load
function, e.g.,
# read in the MyData object
filename = "mydata.pkl"
fp = open(filename, "rb") # open reading from binary file
data = pickle.load(fp)
print(data)
MyData: 'Lab1'
x: [0.7, 0.8, 0.9, 1.0, 1.1]
y: [-10, -9, -8, -7, -6]
Note
To load in a pickled object, the objects class must be available, i.e., defined in the script that you are importing into, or in an importable module, so that it can be reconstructed.
JSON¶
A plain text file format that allows you to save additional meta data is JSON. A JSON file has the format of a dictionary object, so data (numbers, lists, string, or even further dictionaries) stored in a dictionary can be output as a JSON file. Dictionaries have keys and values; therefore the keys can provide information (meta data) about the data that is stored.
The data can be written to a text file using the
dump
function from the built-in
json
Python module:
import json # import json module
# create dictionary to store data
data = {}
data["x values"] = [0.7, 0.8, 0.9, 1.0, 1.1]
data["y values"] = [-10, -9, -8, -7, -6]
data["name"] = "Lab1"
# open file for writing
filename = "mydata.txt"
fp = open(filename, "w")
# write json file
json.dump(data, fp, indent=2) # "indent=2" indents each line by 2 spaces
fp.close()
The output file is human readable and in this case contains:
{
"x values": [
0.7,
0.8,
0.9,
1.0,
1.1
],
"y values": [
-10,
-9,
-8,
-7,
-6
],
"name": "Lab1"
}
A JSON file can be read back in using the
load
function, e.g.,:
import json
# open file for reading
filename = "mydata.txt"
with open(filename, "r") as fp:
data = json.load(fp)
print(data)
{'x values': [0.7, 0.8, 0.9, 1.0, 1.1], 'y values': [-10, -9, -8, -7, -6], 'name': 'Lab1'}
NumPy¶
NumPy can be used to both save and read plain text data, or pickle objects in binary files.
Reading and writing text files¶
Considering our original mydata.txt
file, this could be read in as a NumPy ndarray
using the
loadtxt
function, e.g.,:
import numpy as np
filename = "mydata.txt"
data = np.loadtxt(filename, comments="#")
print(data)
[[ 1. 9.8]
[ 2. 10.3]
[ 3. 12.4]
[ 4. 13.2]
[ 5. 14.7]
[ 6. 16.1]
[ 7. 17.2]
[ 8. 18.7]
[ 9. 20.1]
[10. 21.3]]
Rather than taking a file object, loadtxt
can just be passed the file name. Lines starting with a
particular character, in this case #
, can be ignored using the comments
keyword argument.
If the data file contained columns with values separated by commas (often called comma separated
value, or CSV, files), then the delimiter
keyword argument could be used, e.g., data = np.loadtxt(filename, comments="#", delimiter=",")
.
More control, including converting particular columns to certain data types is available, with the
finest grain of control found using the
genfromtxt
function.
1D and 2D NumPy arrays can be saved to text files using the
savetxt
function, e.g.,:
import numpy as np
# data to save
data = np.array([[0.1, 10.0], [0.2, 11.0], [0.3, 12.0], [0.4, 13.0]])
filename = "mydata.txt"
np.savetxt(filename, data)
The output format (i.e., the number of decimal places on float numbers) can be set using the fmt
keyword argument, e.g., np.savetxt(filename, data, fmt="%.5f")
would output floats with 5 decimal
places. The delimiter between the output values can be set (by default a space), and header and
footer text, preceded by a comment character, can also be set.
Binary files¶
NumPy arrays can be saved as binary files containing pickled data using the
save
function and
then read back in using the
load
function,
e.g.,:
import numpy as np
# our data
data = np.array([[0.1, 10.0], [0.2, 11.0], [0.3, 12.0], [0.4, 13.0]])
# save the data
filename = "mydata.npy" # npy is the standard file extension name
np.save(filename, data)
# read in the data
newdata = np.load(filename)
print(newdata)
[[ 0.1 10. ]
[ 0.2 11. ]
[ 0.3 12. ]
[ 0.4 13. ]]
By default, the save
function assumes you are saving a NumPy array object. However, you can get it to save other Python
objects by explicitly telling it to allow pickling. For example, if you had a simple class like:
class DataPoint:
def __init__(self, x, y, z):
# store copies of x, y and z in the class
self.x = x
self.y = y
self.z = z
and generated a list containing many DataPoint
objects:
import numpy as np
mydata = [DataPoint(x, y, np.sqrt(x ** 2 + y ** 2)) for x, y in np.random.rand(10, 2)]
then to save this you would use:
filename = "mydata.npy"
np.save(filename, mydata, allow_pickle=True)
If you were to then read this data in:
readdata = np.load(filename, allow_pickle=True)
then readdata
will contain the original DataPoint
objects.
Note
The load
function will always load the data as a NumPy array object. So, in the example above, while
mydata
was a list, readdata
will instead be a NumPy array, although it will still contain
the DataPoint
objects.
Pandas¶
While not covered in this tutorial, Pandas is an advanced Python module primarily for holding data tables. It has methods for reading and writing to a variety of file formats including plain text, CSV and Excel spreadsheets.