This page walks through every kind of situation the File I/O topic can throw at you: every mode, the cursor at every position, empty and degenerate files, binary edge cases, and the exam twists that trip people up. We build each example from the ground up, guess the answer first, then verify it.
Forecast: Guess the number of characters before reading on. Count the letters — but don't forget the newlines!
Count the visible letters.apple=5, banana=6, cherry=6 → 17 letters.
Why this step?read() returns the file verbatim as one string; every character on disk counts, including invisible ones.
Add the newlines. There is a \n after each line: 3 newlines. Total = 17+3=20.
Why this step?\n is a single character in the string even though you can't see it. This is the #1 source of "off-by-N" surprises.
Where is the cursor now? At EOF (end of file), because read() consumed everything.
Why this step? This sets up Example 3 — a second read() here would give ''.
Verify: The string is 'apple\nbanana\ncherry\n'. Python's len('apple\nbanana\ncherry\n') = 20. ✓ (checked below)
Forecast: Does the second readline() re-read apple, or continue?
First readline(). Reads up to and including the first \n: a = 'apple\n'.
Why this step?readline() stops the instant it swallows a newline, and the cursor sits right after that newline.
Second readline(). The cursor is now at the start of banana, so it reads b = 'banana\n'.
Why this step? There is one cursor and it persists between calls. The second call does not restart at the top — it continues from where the first stopped. This is the single fact that explains all "surprising" read behaviour.
Verify:a has length 6 (apple=5 + \n), b has length 7 (banana=6 + \n). ✓
Forecast: People expect second to equal first because "the file still exists." Watch what actually happens.
first = f.read() slurps all 20 chars; cursor moves to EOF.
Why this step? Establishes the cursor at the end — the file on disk is untouched, but the finger is now past everything.
second = f.read() returns '' (length 0).
Why this step? There is nothing after the cursor to read. read() reads from the cursor onward, and the cursor is already at the end. The disk file is irrelevant — the cursor position is what matters.
f.seek(0) moves the cursor back to byte 0 (the very start).
Why this step?seek(0) is the "rewind" — it repositions the finger without reopening the file.
third = f.read() now returns all 20 chars again.
Why this step? With the cursor back at the start, read() sees the whole file once more.
Verify: lengths are 20, 0, 20. ✓ (Look at the cursor diagram below — the whole story is where the finger sits.)
Forecast: Is old data still there, before new, after new, or gone?
The instant open(..., "w") runs, the file is truncated to empty.Why this step?w means "start a fresh page." Truncation happens at open time, before you write a single character. old data is gone immediately — you never even got a chance to read it.
f.write("new\n") writes 4 characters (n,e,w,\n) starting at position 0.
Why this step? The cursor for w starts at the beginning of the now-empty file.
Result: file contains 'new\n', length 4. The old 9 characters are unrecoverable.
Forecast: Does more land at the front, the back, or overwrite start?
open(..., "a") does NOT truncate.start\n stays.
Why this step?a = append. It preserves the file and moves the cursor to the end (position 6, just after \n).
f.write("more\n") writes 5 characters at the end.
Why this step? In append mode the cursor is glued to the end, so new text always tacks on after existing text — never over it.
Result:'start\nmore\n', length 6+5=11.
Verify: final content 'start\nmore\n', len = 11. ✓
Each write puts its string down verbatim — nothing extra.Why this step? Unlike print(), write() does not append \n. It writes exactly the characters you gave it and returns how many it wrote (here 2 each time).
The three calls concatenate:'hi' + 'hi' + 'hi' = 'hihihi', all on one line.
Why this step? The cursor sits right after the last character each time, so the next write continues on the same line.
Result:'hihihi', length 6, zero newlines.
Verify: content 'hihihi', len = 6, newline count 0. ✓
Forecast: Does one raise an error and the other return something quiet?
(a) open("ghost.txt", "r") when the file is absent raises FileNotFoundError.
Why this step?rrequires the file to already exist — it never creates one. See Exceptions try-except for the safe way to catch this.
try: with open("ghost.txt", "r") as f: f.read()except FileNotFoundError: print("no such file") # this branch runs
(b) read() on an empty file returns '' — the empty string, no error.
Why this step? An empty file exists; there is simply nothing after the cursor. This is the degenerate boundary of Example 1: zero letters, zero newlines → length 0.
Contrast: missing ⇒ exception; empty-but-present ⇒ quiet ''. These are different!
Forecast: Is header a string of length 8, or something else?
rb opens in binary read mode.read(8) grabs at most 8 bytes.
Why this step? A PNG is not text; decoding those bytes as UTF-8 would crash on byte 0x89. Binary mode does no decoding — you get a raw bytes object. See Bytes vs str and Strings and Encoding (UTF-8).
Length is 8 — each escape like \x89 or \r is one byte, not several characters.
Why this step?\r\n looks like two backslash-sequences but is exactly 2 bytes. Count bytes, not visual symbols.
Type is bytes, not str. The result prints as b'\x89PNG\r\n\x1a\n'.
Why this step? Binary mode always yields bytes; text mode always yields str. Mixing them (f.write("text") on a b file) raises TypeError.
Verify:len(b'\x89PNG\r\n\x1a\n') = 8 and its type is bytes. ✓
Forecast: For Part 1, guess the ERROR count. For Part 2, does XY insert (pushing digits right) or overwrite?
Part 1 — never readlines() on 10 GB.Why this step?readlines() builds a list holding the entire file in RAM at once → out-of-memory crash. for line in f: reads one line at a time (lazy iteration, see Generators and Lazy Iteration) → constant memory.
Count with the lazy loop on small.log:
count = 0with open("small.log") as f: for line in f: if line.startswith("ERROR"): count += 1# count == 2
Why this step? Two lines begin with ERROR (ERROR disk full, ERROR timeout); ok does not. The loop touches each line once and forgets it — that's why it scales to 10 GB.
Part 2 — r+ opens for read AND write with NO truncation. The cursor starts at position 0.
Why this step?r+ is how you edit an existing file without wiping it (unlike w). The file must already exist.
f.seek(3) moves the cursor to index 3 (the 4th character). f.write("XY")overwrites the two characters at indices 3 and 4.
Why this step? Writing to a file does not insert and push text right — it overwrites byte-for-byte from the cursor. Two written chars replace two existing chars.
Result: original 0000000000 → positions 3,4 become X,Y → '000XY00000', still length 10.
Verify:count = 2; final content '000XY00000', len = 10. ✓