Reading and Writing Files

Reading Text Files


Learning Objectives

  • You can open a file safely with the with statement and explain why the with form matters.
  • You can choose between reading a whole file at once, reading line by line, and getting a list of lines.
  • You can use .split() to break a line of text into fields.
  • You can specify a file’s encoding and explain why being explicit is useful.
  • You can handle a missing file by catching FileNotFoundError.

The job of the next several chapters is to teach the program to remember things between runs — to read information that was saved before, and write information that the next run can read back. This chapter is the read side.

A file is, at the simplest level, a sequence of characters stored on disk. Python can open it, look at the characters, and let you do whatever you want with them.

A disk is a physical device on your computer that stores data. It is called a disk because the original ones were spinning disks coated with magnetic material. Modern disks are often solid-state, with no moving parts, but the name stuck. A file is a named collection of data on the disk. The file system is the part of the operating system that manages files — keeping track of where they are, what they are called, and who can access them.

Opening a File With with

Here is the smallest useful example. Suppose you have a file called greeting.txt in your project folder, containing the single line:

Hello from a file.

You can read it like this:

with open("greeting.txt") as f:
contents = f.read()
print(contents)

Three things happen on that first line. open("greeting.txt") opens the file. as f gives the open file a name, f, that you use inside the block. And with makes sure that when the block ends — normally or because something went wrong — the file is properly closed.

The with part deserves attention. Files are a resource the operating system manages, and a program that opens files without closing them is a program that quietly accumulates problems. The with statement closes the file for you automatically, even if an exception is raised inside the block. Without with, you would have to remember to call f.close() yourself, and you would have to remember to call it even on the error path.

There is a Pythonic phrasing for this kind of construct: a context manager. with is the keyword that uses one. You will not see them often in this course outside of file handling, but with open(...) as f: is the canonical example and is worth memorising in this shape.

Three Ways to Read

f.read() returns the entire file as a single string. That is fine for small files. For larger files, or for files that have one record per line, two other methods are better suited.

f.readlines() returns a list of strings — one per line in the file:

with open("greeting.txt") as f:
lines = f.readlines()
print(lines)
# ['Hello from a file.\n']

Two things to notice. The result is a list, which means you can index into it (lines[0]) and iterate over it the usual way. And each string has a trailing \n — the newline character that marked the end of the line in the file. The newline is part of the line as far as Python is concerned, and you usually want to remove it before doing anything else with the line.

The third way is to iterate the file directly:

with open("greeting.txt") as f:
for line in f:
print(line)

This reads one line at a time, which is much more memory-efficient for large files: the whole file is never held in memory at once. The trade-off is small for the file sizes you will work with in this course, but the line-by-line form is the most general and is what most file-reading code looks like in practice.

MethodWhat you getWhen to use
f.read()The whole file as one stringSmall files; you want the content as a single chunk
f.readlines()A list of all the linesYou want random access to lines, or you need the count
for line in f:One line at a timeThe default; works for any size; the most idiomatic

Reading line by line

0 / 5 points

Why is for line in f: usually a good default for processing records in a text file?

Newlines Are Part of the Line

Files store text as a stream of characters, and Python does not strip the newlines for you. A file with three lines is, as far as Python is concerned, a string like "first\nsecond\nthird\n". When you read it line by line, each line still has the \n at the end.

This means a print of a freshly-read line tends to produce two blank-looking gaps — one from the file’s newline, one from print’s newline. The fix is .strip() that removes whitespace (including newlines) from both ends:

with open("subjects.txt") as f:
for line in f:
clean = line.strip()
print(f"Read: {clean}")

Breaking a Line into Fields With .split()

Sometimes a line in a file is not just text — it is several values packed together with a separator between them. A file of study sessions might look like:

Math,45
Physics,90
History,30

Each line is a subject and a number of minutes, separated by a comma. To work with the values, you need to split the line into two pieces.

This is what the .split() string method does. It takes a separator and returns a list of the parts:

line = "Math,45"
parts = line.split(",")
# parts is now ["Math", "45"]
print(parts[0]) # Math
print(parts[1]) # 45

.split(",") splits at every comma. You get a list with as many elements as there were pieces — two for "Math,45", three for "a,b,c", and so on.

Put this together with line-by-line reading, and you have the basic shape of “read a file of records”:

with open("sessions.txt") as f:
for line in f:
parts = line.strip().split(",")
subject = parts[0]
minutes = int(parts[1])
print(f"{subject}: {minutes} minutes")

Three habits combine. .strip() removes the trailing newline so it does not contaminate the last field. .split(",") breaks the line into parts. int(...) converts the minutes from text to a number, since everything read from a file starts as text.

Split with separator

0 / 5 points

What is the best description of "A|25".split("|")?


Split Without an Argument

With no argument, .split() splits on any run of whitespace — spaces, tabs, newlines — and collapses multiple whitespace characters into one separator. With an argument, it splits on exactly that character, every time, even if two of them are next to each other:

"a,,b".split(",")
# ['a', '', 'b'] -- the empty string between the two commas is a real "piece"
"a b".split(" ")
# ['a', '', '', 'b'] -- three "empty" pieces between the a and the b

The no-argument form is the right tool for human-written text with irregular spacing. The argument form is the right tool for delimited data, where every separator counts as a real boundary and an empty field is a meaningful value.

What If the File Does Not Exist?

If the file is not where Python expects it, open(...) raises a FileNotFoundError:

Traceback (most recent call last):
File "main.py", line 1, in <module>
with open("missing.txt") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'

This is just an exception, and you already know how to handle exceptions: the exceptions chapter from the previous part introduced try/except. The pattern there applies directly here:

try:
with open("sessions.txt") as f:
for line in f:
print(line.strip())
except FileNotFoundError:
print("No sessions file yet -- starting fresh.")

Two things are worth noticing. First, except FileNotFoundError: catches only that specific exception — nothing else gets swallowed. Second, the except block does something meaningful: it prints a helpful message instead of crashing. A bare except: or except: pass would be wrong here for the reasons the previous part already covered.

This is also the first hint of an idea that becomes important in the project chapter: a missing file is sometimes expected. The very first time you run your study log, no sessions.txt exists yet — and “no file yet” is exactly the right state for a new install. It is not an error; it just means “no data yet, start empty.”

Encoding

Files store characters as bytes — numbers — on disk, and the rules for translating between bytes and characters are called an encoding. Most of the time you do not have to think about this, because Python picks a sensible default, but the default can vary depending on your operating system.

To avoid surprises later, the recommendation throughout this course is to always specify the encoding explicitly:

with open("greeting.txt", encoding="utf-8") as f:
contents = f.read()

The default in most modern systems is utf-8, but it is not guaranteed to be the same everywhere. By being explicit, you make your code work the same way on every machine, and you avoid the possibility of a file that works on your computer but raises UnicodeDecodeError when someone else tries to read it.

utf-8 is the encoding that handles every character you are likely to need — regular Latin letters, accented letters, characters from other scripts, emoji.

For now, take encoding="utf-8" as a habit. It will save you trouble later. We’ll look in a bit more detail at what happens when you get the encoding wrong in Defensive Programming with Files and External Data — the short version is that you get a UnicodeDecodeError, and the fix is usually to be explicit about the encoding in the first place.

Where the File Lives

When you write open("sessions.txt"), Python looks for sessions.txt in the working directory — the folder the terminal is currently in, not necessarily the folder where your .py file lives. This is the same idea introduced in Working on Your Own Computer: the working directory is where the terminal currently sits, and file paths in your code are interpreted relative to that.

The practical consequence is: if you keep your .py files and your data files in the same folder, and you run your programs from that folder, the simple form open("sessions.txt") will work. If you ever see FileNotFoundError on a file you know is there, the first thing to check is what folder the terminal is in.

The sequence above splits file reading into small pieces: clean the lines, parse one line, then combine the pieces into a complete reader. Now try the same overall pattern as one compact helper where the reading, cleaning, parsing, and collecting happen inside a single function.

Read book minutes from a text file

0 / 15 points

Define a function named read_book_minutes(path).

The file at path contains one book record per line. Each valid line has this exact format:

title|minutes

For example:

Python Basics|45
Data Stories|30

The function must:

  • open the file with with open(path, encoding="utf-8") as f,
  • read the file line by line,
  • remove the trailing newline and surrounding whitespace from each line,
  • skip blank lines,
  • split each non-blank line at the | character,
  • convert the minutes field to an integer,
  • and return a list of rows where each row is [title, minutes].

If a line has extra spaces around the title or minutes, strip those spaces. For example, Poetry | 20 becomes ["Poetry", 20].

At the bottom of the program, call read_book_minutes("books.txt") and print the returned list.

Example books.txt content:

Python Basics|45
Data Stories | 30
Poetry| 20

Required output text:

[['Python Basics', 45], ['Data Stories', 30], ['Poetry', 20]]

The tests will also call read_book_minutes(...) directly with their own temporary files.

Check Your Understanding

  1. Why is with open(...) as f: preferred over calling open(...) directly?
  2. What is the difference between f.read(), f.readlines(), and iterating over f with a for loop?
  3. What is the difference between .split(",") and .split() with no argument?
  4. If open("data.txt") raises a FileNotFoundError, is it always a problem?
  5. Why does the course recommend always passing encoding="utf-8" to open(...)?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0