Chapter 2

Reading Text

Chapter 1 handed you the whole file as one string with f.read(). That's occasionally what you want, and often not. Text files are usually made of lines, and most jobs work on one line at a time: count them, filter them, pull a value out of each. This chapter is the ways to get at a file's text in the shape you actually need.

Everything here is still plain text. The reading log looks like a spreadsheet, but to Python it's a run of characters with newlines in it, the same as a note.

The whole file at once

f.read() with no argument returns everything from the current position to the end, as a single string. Use it when the file is small and you genuinely need all of it together, for example to search the text for a word:

with open("notes/piranesi.txt") as f:
    note = f.read()

print("Mentions the journal:", "journal" in note)
print("Lines in the note:", len(note.splitlines()))
Output
Mentions the journal: True
Lines in the note: 11

note is one string. The line breaks are still in there as \n characters, and note.splitlines() cuts the string on them into a list.

The catch is size. f.read() pulls the entire file into memory. For a note that's nothing. For a log file that's been growing for two years it's a problem, and the last section of this chapter is about what to do instead.

One line at a time

A file object is iterable. Loop over it and you get its lines, one per turn:

with open("reading-log.csv") as f:
    line_count = 0
    for line in f:
        line_count += 1

print(f"{line_count} lines, so {line_count - 1} books")
Output
11 lines, so 10 books

This is the form you'll reach for most. It reads one line, gives it to you, then reads the next, so it never holds more than a single line in memory no matter how big the file is.

A line here means the text up to and including the next newline character. That last part matters more than you'd think.

The trailing newline

Each line the loop gives you keeps its \n on the end. You can see it with repr, which shows the characters the way Python would write them:

with open("reading-log.csv") as f:
    lines = f.readlines()

print(repr(lines[0]))
print(repr(lines[1]))
Output
'title,author,rating,finished\n'
'The Left Hand of Darkness,Ursula K. Le Guin,5,2025-11-02\n'

If you print a line as-is, the \n in the string plus the newline print adds gives you a blank line between every row. The fix is str.rstrip, which removes trailing whitespace. Pass it "\n" to remove only the newline and leave any real trailing spaces alone:

with open("reading-log.csv") as f:
    for line in f:
        print(line.rstrip("\n"))
Output
title,author,rating,finished
The Left Hand of Darkness,Ursula K. Le Guin,5,2025-11-02
Project Hail Mary,Andy Weir,4,2026-01-15
"Rendezvous with Rama, and Other Stories",Arthur C. Clarke,4,2026-02-20
One Hundred Years of Solitude,Gabriel Garcia Marquez,5,2026-03-30
The Peripheral,William Gibson,3,2026-05-11
Klara and the Sun,Kazuo Ishiguro,4,2026-06-01
Piranesi,Susanna Clarke,5,2026-07-19
Recursion,Blake Crouch,,
A Memory Called Empire,Arkady Martine,4,2026-08-25
The Three-Body Problem,Liu Cixin,,

Bare line.strip() with no argument also works and is common, but it strips leading whitespace too, which isn't always what you want. When in doubt, rstrip("\n") does exactly one thing.

readline and readlines

Two more methods, both occasionally the right tool.

f.readline() reads a single line and advances the position. It's useful when the first line is special, like a header, and you want to handle it before the loop:

with open("reading-log.csv") as f:
    header = f.readline()
    print("Columns:", header.rstrip("\n"))

    books = 0
    for line in f:
        books += 1
    print("Books:", books)
Output
Columns: title,author,rating,finished
Books: 10

The for loop picks up from line two, because readline already consumed line one.

f.readlines() reads the whole file and returns a list of lines. It costs the same memory as f.read(), plus the overhead of the list. Reach for it only when you truly need all the lines as a list at once, for instance to walk them in reverse. For everything else, the plain for line in f loop is lighter.

Pulling a value out of a line

So far we've worked with whole lines. Usually you want one piece of a line: the title, or the rating. The obvious move is to split the line on commas:

with open("reading-log.csv") as f:
    next(f)  # skip the header
    for line in f:
        title = line.split(",")[0]
        print(title)
Output
The Left Hand of Darkness
Project Hail Mary
"Rendezvous with Rama
One Hundred Years of Solitude
The Peripheral
Klara and the Sun
Piranesi
Recursion
A Memory Called Empire
The Three-Body Problem

Look at the third line. That book's title is Rendezvous with Rama, and Other Stories. It has a comma in it, so the file wraps it in quotes, and line.split(",") cut it in half at the wrong comma.

Reading a file line by line is fine. Carving those lines into fields by hand is where it goes wrong, and it goes wrong quietly. Chapter 7 is about the csv module, which handles the quoting and gives you the fields correctly. Until then, stick to whole lines.

(next(f) there is another way to skip the header: it asks the file object for its next line and throws it away.)

Big files: read what you need, not the whole thing

The for line in f loop is the default because of what comes next. Build a log with a hundred thousand rows in it:

with open("big-log.csv", "w") as f:
    f.write("title,author,rating,finished\n")
    for n in range(100_000):
        f.write(f"Book {n},Author {n},{n % 6},2026-06-01\n")

Now count how many are rated 5, without ever holding more than one line:

rated_five = 0
with open("big-log.csv") as f:
    next(f)  # skip the header
    for line in f:
        fields = line.rstrip("\n").split(",")
        if fields[2] == "5":
            rated_five += 1

print(f"{rated_five} books rated 5")
Output
16666 books rated 5

The generated titles have no commas in them, so split(",") is safe here. The point is the loop. It reads one line, checks it, discards it, and moves on. Swap that loop for f.read() or f.readlines() and you'd load the whole file, which is fine at a hundred thousand rows and falls over at a hundred million.

Reading a fixed-size chunk with f.read(size) in a loop is another option for files that aren't line-based at all, like binary data. Chapter 10 covers that.

Common mistakes

Reading the same file twice. After f.read(), the position is at the end of the file. Read again and you get an empty string:

with open("notes/piranesi.txt") as f:
    first = f.read()
    second = f.read()

print(len(first) > 0, "then", len(second))
Output
True then 0

The same thing bites you if you f.read() and then try to loop the file: the loop starts from the end and yields nothing. Read the file once, into a variable, and work with the variable.

Using readlines "to be safe". for line in f and f.readlines() look similar, but only the loop is memory-safe on a large file. There's no safety in readlines; there's a hidden cost.

Forgetting the header row. If the first line of the file is column names, it flows through your loop like any other line. Skip it with next(f) or f.readline() before the loop, or check for it inside.

Practice

Try each of these before you read the solution under it.

  1. Loop over reading-log.csv and print the title of every book you haven't finished. Those are the rows where the last field is empty, so the line ends with a comma before its newline.
  2. Write first_lines(path, n) that returns the first n lines of a file as a list, stripped of their newlines, without reading the whole file.
  3. Count how many books are finished and how many are still in progress, reading the file only once.

Solutions

1. Strip the newline, then check whether the line ends with a comma.

with open("reading-log.csv") as f:
    next(f)  # skip the header
    for line in f:
        if line.rstrip("\n").endswith(","):
            print(line.split(",")[0])
Output
Recursion
The Three-Body Problem

2. Stop the loop once you have n lines. The file is never read past that point.

def first_lines(path, n):
    lines = []
    with open(path) as f:
        for line in f:
            lines.append(line.rstrip("\n"))
            if len(lines) == n:
                break
    return lines

for line in first_lines("reading-log.csv", 3):
    print(line)
Output
title,author,rating,finished
The Left Hand of Darkness,Ursula K. Le Guin,5,2025-11-02
Project Hail Mary,Andy Weir,4,2026-01-15

3. One pass, one counter each.

finished = 0
reading = 0
with open("reading-log.csv") as f:
    next(f)
    for line in f:
        if line.rstrip("\n").endswith(","):
            reading += 1
        else:
            finished += 1

print(f"{finished} finished, {reading} still reading")
Output
8 finished, 2 still reading

Where this leaves you

You can read a file whole, line by line, or a piece at a time, and you know why the line-by-line loop is the one to reach for by default. You've also seen the first crack in splitting lines by hand, which Chapter 7 will deal with properly. Chapter 3 is the other direction: writing text back out to a file.