1.3.3 · D5Python Intermediate
Question bank — File I - O — open modes (r, w, a, rb), read, readline, readlines, write
First: picture the cursor (the one idea behind every trap)
Before any trap makes sense, you need one picture in your head.
Look at the figure below. The teal finger is the cursor. In r and w it starts at the very start; in a (append) it starts at the end, past all existing content. The orange shading marks what w destroys the instant it opens.


Defaults and the letters people never type
True or false — justify
Opening "data.txt" with mode w and then immediately calling f.read() returns the old contents.
False.
w truncates the file to empty the instant it opens, so there is nothing to read and w is write-only, so read() raises io.UnsupportedOperation besides.Opening with mode a moves the cursor to the start of the file.
False.
a (append) puts the cursor at the end (see the teal finger in figure 1), so every write lands after existing content; that is the whole point of "append."f.write("hello") writes hello followed by a newline.
False.
write writes the string verbatim and adds nothing; three write("hi") calls produce hihihi. You must add \n yourself.f.readlines() returns lines with the trailing \n stripped off.
False. Each string in the list keeps its
\n (except possibly the last line if the file didn't end in one). Strip with rstrip("\n") if you want clean text.After f.read() reaches end-of-file, calling f.read() again raises an error.
False. It quietly returns
'' (empty string). No exception — the cursor just sits at EOF with nothing left to hand you.open("x.txt", "r") will create x.txt if it doesn't exist.
False.
r requires the file to already exist and raises FileNotFoundError if it's missing. Only w, a, and x create files.open("x.txt") with no mode argument opens for writing.
False. The default mode is
"r" — read text. To write you must pass "w", "a", or a + mode explicitly.Text mode and binary mode return the same type of object from read().
False. Text mode returns
str (decoded via an encoding); binary mode (b) returns bytes. See Bytes vs str — they are different types and don't mix.The with statement is just cosmetic; open() and f.close() do exactly the same job.
False.
with guarantees close() runs even if an exception is raised mid-block, whereas a bare close() is skipped on error. That flush-and-release safety is the point of Context Managers (with statement).f.read(5) always returns exactly 5 characters.
False. It returns at most 5; near EOF it returns fewer, and at EOF it returns
''. The n is a ceiling, not a promise.Mode r+ truncates the file like w does.
False.
r+ opens for read and write with the cursor at the start and does not truncate; the file must already exist. Only w/w+ wipe on open.In text mode, the two-byte Windows line ending \r\n reaches your Python string as two characters.
False. Text mode does universal newline translation: on read, any
\r\n or \r becomes a single \n; on write, \n becomes the platform ending. You only see the raw \r\n in binary mode.The plus modes side by side
Which plus mode lets you read old contents and keep them while adding new data at the end?
==
a+.== It doesn't truncate and its writes always append; you can seek(0) to read the old text, then write, and the write still lands at the end.Why does w+ return '' from a read() immediately after opening an existing file?
Because
w+ truncated the file to empty on open — the + added read capability but the w still wiped everything, so there is nothing to read.Between r+ and w+, which is safe to open a config file you want to edit in place?
==
r+== — it preserves the existing contents (no truncation), letting you read, seek, and overwrite selectively. w+ would destroy the file before you saw it.Spot the error
with open("log.txt", "w") as f:
old = f.read() # want to see previous log
f.write("new\n")
``` — what's wrong? ::: `w` truncated `log.txt` to empty *before* the read, and `w` is write-only anyway, so `read()` raises. To read old data then add, use `"a"` (append), `"r+"` (read then write), or `"a+"` (read old *and* append).
```python
f = open("notes.txt")
data = f.read()
process(data)
``` — what's the hidden risk? ::: The file is never closed; if `process` raises, `f.close()` never runs and the handle leaks (and any writes stay buffered). Wrap it in `with open(...) as f:` so it always closes.
```python
with open("out.txt", "w") as f:
f.writelines(["a", "b", "c"])
# expecting a, b, c on separate lines
``` — why does the output look wrong? ::: `writelines` adds **no** newlines, so the file contains `abc` on one line. Provide the newlines yourself: `["a\n", "b\n", "c\n"]`.
```python
with open("pic.png", "r", encoding="utf-8") as f:
data = f.read()
``` — why can this crash? ::: A PNG is raw bytes, not UTF-8 text; decoding it will hit invalid byte sequences and raise `UnicodeDecodeError`. Open images with `"rb"` to get `bytes` — no decoding attempted.
```python
with open("a.txt") as f:
text = f.read()
lines = f.readlines() # lines is empty!
``` — why is `lines` empty? ::: `read()` already moved the cursor to EOF, so `readlines()` finds nothing left. There is **one** cursor; earlier reads consume the data. Call `f.seek(0)` before the second read.
```python
with open("data.bin", "wb") as f:
f.write("hello")
``` — why does this fail? ::: In binary mode `write` expects `bytes`, but `"hello"` is a `str`; this raises `TypeError`. Encode first: `f.write("hello".encode("utf-8"))`.
```python
with open("names.txt") as f: # no encoding given
text = f.read()
``` — why can this read garbled characters on another machine? ::: With no `encoding=`, Python uses the **system locale** default, which may not be UTF-8; a file saved as UTF-8 can decode wrongly (mojibake) or raise. Always pass `encoding="utf-8"`.
---
## Why questions
Why does `open()` demand a mode *up front* instead of letting you decide read-vs-write later? ::: The OS must know your intent before it hands out the handle — whether to create/truncate the file, where to place the cursor, and what permissions to request. That decision can't be deferred.
Why does `w` wipe the file the *moment* you open it, not when you first write? ::: Truncation is part of "open for writing from scratch," so the OS empties the file as it grants the handle. Even if you never call `write`, the old contents are already gone.
Why does `readline()` keep the `\n` at the end of each line? ::: So you can tell a real line break apart from the final line that had no newline, and so re-writing the lines reproduces the file exactly. Stripping is your choice, not the reader's default.
Why is `for line in f:` preferred over `f.readlines()` for a 10 GB file? ::: The `for` loop reads **lazily**, one line at a time, keeping only one line in RAM, while `readlines()` loads the whole file into a list at once. See [[Generators and Lazy Iteration]].
Why does binary mode skip the encoding step entirely? ::: Encoding turns text characters into bytes and back; but binary data (image, zip) has no "characters," so decoding would corrupt or crash it. `b` gives you the raw bytes untouched, per [[Strings and Encoding (UTF-8)]].
Why does text mode translate newlines but binary mode does not? ::: Text mode's job is to hide platform quirks so `\n` means "new line" everywhere; binary mode must preserve every byte exactly, so translating `\r\n` would corrupt files. That's why you read `\r\n` as-is only under `b`.
Why does appending with `a` never risk overwriting old lines, no matter how much you write? ::: The cursor starts at end-of-file and every write extends the file forward; there is no existing content in front of the cursor to overwrite. Contrast `r+`, whose start-cursor *would* overwrite.
Why should you wrap file work in `try-except` or `with` even when the path looks correct? ::: Files can vanish, permissions can be denied, or disks can fill *between checks and use*; only runtime handling catches these. See [[Exceptions try-except]] and [[os and pathlib]] for the failure modes.
---
## Edge cases
What does `f.read()` return on a brand-new empty file opened with `r`? ::: The empty string `''` — there's nothing to read, but that's not an error; the cursor is already at EOF.
If a file's last line has no trailing `\n`, what does `readlines()` give for it? ::: The last element has **no** `\n` (e.g. `['a\n', 'b']`), because the reader returns exactly what's there and there was no newline to include.
What happens if you open an existing file with `"x"` (exclusive create)? ::: It raises `FileExistsError`. `x` succeeds **only** if the file does not already exist, giving you a safe "create or fail" guarantee.
Reading a 3-byte file with `f.read(1000)` in binary mode returns how many bytes? ::: All 3 bytes and no more — `n` is an upper bound, and there simply aren't 1000 bytes to give.
After you `f.seek(0)` on a file opened with `a`, does the *next write* go to the start? ::: No — files opened in append mode force every write to the end on most platforms, regardless of where you seek. Seeking only affects reads in `a+`, not the write position.
Calling `f.write("x")` returns what, and why is that occasionally useful? ::: It returns the number of characters written (here `1`); you can use it to confirm a full record was written, though for text it's usually just the length.
On Windows, if you write `"a\nb\n"` in text mode, how many bytes land on disk? ::: Six — each `\n` is translated to the two-byte `\r\n`, so `a\r\nb\r\n`. In binary mode you'd get four bytes, `\n` untranslated.
---
> [!recall]- One-sentence summary of every trap
> There is **one cursor** (reads consume it, `w`/`a` decide where it starts), `w`/`w+` **wipe on open**, `write` **adds no newline**, `readlines` **keeps the `\n`**, text mode **normalises newlines and needs an explicit `encoding`**, binary means **bytes with no decoding**, and `with` is the only close you can trust.
> [!mnemonic] The three questions every trap tests
> **"Where's the cursor? What's the type? Who closed it?"** — cursor position explains re-reads and appends, type (str vs bytes, and which encoding) explains decoding crashes, and closing explains lost buffered writes.