1.3.3 · D4Python Intermediate

Exercises — File I - O — open modes (r, w, a, rb), read, readline, readlines, write

2,338 words11 min readBack to topic

The exercises share one imaginary file. Picture it as a stack of lines, each ending in an invisible \n. The cursor is your finger on that stack.

Figure — File I - O — open modes (r, w, a, rb), read, readline, readlines, write

Our running file fruit.txt holds exactly these bytes:

apple\nbanana\ncherry\n

That is three lines, each terminated by \n. Whenever a problem says "fresh file", assume the cursor sits at the very start (top of the stack).


Level 1 — Recognition

Goal: read a mode and state what it does, with no code running.

Recall Solution L1.1

(a) "r" — read-only, file must already exist, cursor at start. (b) "w" — truncates to empty on open (or creates), cursor at start. (c) "a" — preserves contents, cursor jumps to the end, so writes append. (d) "rb" — binary read, hands you bytes, no decoding, no newline translation.

Why: every mode answers three questions at once — must-exist? truncate? cursor where? Match the intent to those three switches. See Bytes vs str for why .png needs b.

Recall Solution L1.2

"r" → a str (text, decoded via an encoding like UTF-8). "rb" → a bytes object (raw, undecoded). The b flag is the only thing that flips text↔bytes. See Strings and Encoding (UTF-8).


Level 2 — Application

Goal: run the readers/writers in your head and predict exact strings.

Recall Solution L2.1
  • a = 'apple\n'readline() reads up to and including the first \n, then stops. Cursor now sits at the b of banana.
  • b = 'ban'read(3) grabs exactly 3 characters from where the cursor is. Cursor now between ban and ana.
  • c = 'ana\ncherry\n' — plain read() slurps everything remaining and lands at EOF.

Key idea: there is only one cursor and every read advances it. The three calls chain forward; they never restart.

Recall Solution L2.2

n == 5write returns the number of characters written, and "hello" is 5 characters. out.txt contains exactly hellono trailing newline, because write adds nothing.

Recall Solution L2.3

The file contains abc — one run, no separators. writelines writes each string verbatim and inserts no newlines. If you wanted lines you had to include them yourself: ["a\n", "b\n", "c\n"].


Level 3 — Analysis

Goal: explain surprising behaviour by reasoning about the cursor.

Recall Solution L3.1

first = 'apple\nbanana\ncherry\n' (the whole file). second = '' (empty string). Why: the first read() moved the cursor to EOF (end of file). There is nothing after EOF, so the second read() returns the empty string. The file on disk is untouched — it's the cursor position that changed. To re-read, call f.seek(0) to rewind, or reopen.

Recall Solution L3.2
with open("fruit.txt") as f:
    clean = [ln.rstrip("\n") for ln in f.readlines()]
# clean == ['apple', 'banana', 'cherry']

readlines() returns ['apple\n', 'banana\n', 'cherry\n'] — each line keeps its trailing \n because a "line" by definition includes its terminator. We use rstrip("\n") (strip only trailing \n, not spaces) to peel it off. Using bare rstrip() would also eat trailing spaces, which is sometimes wrong.

Recall Solution L3.3

After the "a" block: apple\nbanana\ncherry\ndate\n — append opens with the cursor at the end, so date lands after cherry. After the "w" block (run on that result): date\nw truncated the file to empty on open, destroying all four lines, then wrote date\n. That is the entire difference between a and w.


Level 4 — Synthesis

Goal: combine modes, cursors, and safety into a small real task.

Recall Solution L4.1
with open("fruit.txt") as src, open("fruit_upper.txt", "w") as dst:
    for line in src:                 # lazy: one line at a time
        dst.write(line.upper())      # keeps the existing \n
# fruit_upper.txt == 'APPLE\nBANANA\nCHERRY\n'

Why each piece:

  • Two open()s in one with → both files auto-close/flush even on error (see Context Managers (with statement)).
  • for line in src: iterates lazily, so a 10 GB file uses tiny memory (see Generators and Lazy Iteration).
  • line already ends in \n; .upper() leaves that \n alone, so we don't add or remove newlines.
Recall Solution L4.2
lines = 0
chars = 0
with open("fruit.txt") as f:
    for line in f:
        lines += 1
        chars += len(line)   # len counts the trailing \n too
# lines == 3, chars == 20

Character math: apple\n=6, banana\n=7, cherry\n=7 → 20 characters, 3 lines. One pass, one line in memory at a time.

Recall Solution L4.3
def is_png(path):
    with open(path, "rb") as f:      # binary: bytes, no decoding
        return f.read(8) == b"\x89PNG\r\n\x1a\n"

Why rb: a PNG is not text; decoding its bytes as UTF-8 would raise a UnicodeDecodeError. rb hands you raw bytes, which we compare against the known 8-byte signature (see Bytes vs str).


Level 5 — Mastery

Goal: reason across cursor, encoding, exceptions, and all edge cases.

Recall Solution L5.1
  • a = 'apple\nbanana\ncherry\n' — full slurp, cursor at EOF.
  • f.seek(0) — cursor jumps back to position 0 (the top of the stack).
  • b = 'apple\n' — first line from the rewound start.
  • c = 'banana\ncherry\n' — everything after the first line, since the cursor is now just past apple\n. seek(0) is the standard way to re-read without reopening.
Recall Solution L5.2
def safe_read(path):
    try:
        with open(path, encoding="utf-8") as f:
            return f.read()
    except FileNotFoundError:
        return ""

Why: mode r requires the file to exist; a missing file raises FileNotFoundError. We catch only that specific exception (not a bare except, which would also swallow bugs) and return "". See Exceptions try-except and os and pathlib for existence checks.

Recall Solution L5.3
  • Exists: raises FileExistsErrorx means "create only if new", so it refuses to touch an existing file. This protects you from accidental overwrites that w would do silently.
  • Does not exist: creates the file, opens it for writing, cursor at start — behaving like w on a brand-new file. Use x when "must not already exist" is a hard requirement (e.g. writing a unique output).
Recall Solution L5.4
  • p = 'apple\n' — one line; cursor now before banana.
  • q = ['banana\n', 'cherry\n']readlines() grabs all remaining lines (not the already-read apple) into a list, each keeping \n. Cursor now at EOF.
  • r = '' — nothing left after EOF, so read() returns the empty string. Lesson: all three readers share and advance the same cursor; each starts where the last stopped.

Active recall

Answer before revealing:

seek(0) :::: rewinds the cursor to the start so you can re-read without reopening. The exclusive-create mode :::: x — it raises FileExistsError if the file already exists. Does writelines add newlines? :::: No — it writes each string verbatim; you must include \n yourself. Why rb gives bytes :::: binary mode does no decoding, so it hands back raw bytes, not decoded str.