Session 19 of the Python Fundamentals series, covered in depth. It reads and writes real text files, covering open(path, mode), the with statement, whose context manager always closes the file, and the reading options: .read(), .readline(), iterating with for line in f, and .readlines(). It then covers writing with 'w', which truncates, against appending with 'a', stripping the trailing newline, and encoding='utf-8'. The traps are that 'w' silently erases the file, that a file used after it is closed will fail, that reading the same file object twice returns an empty string the second time, forgetting to strip the newline, writing a non-string, and the FileNotFoundError you get for a missing path in 'r'. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 108 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 19
Read and write real files on disk - and close them safely
Objectives
Until now your data vanished when the program ended. Files let a program remember - save results, load them back, keep a log. By the end you can:
open(path, mode) and choose the right mode..read(), .readline(), a for loop, or .readlines().'w' and append with 'a' - and know which one erases.with open(...) as f: so the file always closes.encoding='utf-8'.FileNotFoundError and the read-twice-gives-empty trap.Warm-up
Discussion prompt
Before we open Session 19 - Files & Text I/O: without looking back, what was the main idea of Session 18 - Mutability & Aliasing, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 18 of the Python Fundamentals series, in depth. The hidden-bug session: a name is a label bound to an object, so b = a makes two names for ONE list - mutating through one shows through the other.
Section
Part 1
Concept
A text file is just a sequence of characters saved on disk - it outlives your program. To use one, you first open it, which gives you a file object to read from or write to.
file object — The value open() hands back. You call methods on it (.read(), .write()) and then close it.
Counterexample
Discussion prompt
A text file is just a sequence of characters saved on disk - it outlives your program. To use one, you first open it, which gives you a file object to read from or write to.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
open takes a path (the file name) and a mode (what you intend to do). The mode is a short string: read, write, or append.
| mode | means | if file is missing | existing content |
|---|---|---|---|
| 'r' | read (default) | FileNotFoundError | kept |
| 'w' | write | creates it | ERASED |
| 'a' | append | creates it | kept, adds at end |
Comparison
Comparison matrix
From open(path, mode) picks the job: refill the means column from what you know. The rest of the table is as it appeared.
| mode | means | if file is missing | existing content |
|---|---|---|---|
| 'r' | read (default) | FileNotFoundError | kept |
| 'w' | write | creates it | ERASED |
| 'a' | append | creates it | kept, adds at end |
Ranking
Put in order
Put the moves of Open, write one line, close into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Mode 'w' creates the file (or empties it) and hands back a file object f.
Worked example
f = open("data.txt", "w")
f.write("hello\n")
f.close()
print("done")Line 1 opens data.txt for writing
Why: Mode 'w' creates the file (or empties it) and hands back a file object f.
Line 2 writes the text
Why: "hello\n" is stored; the \n ends the line. Line 3 closes the file, flushing it to disk.
Read the output
Why: Verified by execution: prints done. The file now holds one line.
| step | result |
|---|---|
| open(..., 'w') | file object f |
| f.write("hello\n") | 5 chars written |
| f.close() | saved to disk |
| done |
Trade off
Comparison matrix
From Open, write one line, close: every row here is a choice with a cost. Fill the result column, then say which row you would actually pick and what you give up for it.
| step | result |
|---|---|
| open(..., 'w') | file object f |
| f.write("hello\n") | 5 chars written |
| f.close() | saved to disk |
| done |
Concept
An open file holds a system resource, and writes may sit in a buffer until you close it. Forget to close and your data can be left unsaved or the file stays locked.
You can call f.close() by hand - but if the code crashes first, it never runs. There is a safer way.
Analogy
Discussion prompt
Explain Closing matters by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
An open file holds a system resource, and writes may sit in a buffer until you close it. Forget to close and your data can be left unsaved or the file stays locked.
Intuition
Opening a file is like pulling open a drawer: you can now put things in or take things out. But you should close the drawer when you are done.
Leave drawers open all over the house and things spill. Leave files open and writes may never reach the disk. The next part closes the drawer for you automatically.
Explain it
Discussion prompt
Explain Opening a file is like opening a drawer to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Opening a file is like pulling open a drawer: you can now put things in or take things out. But you should close the drawer when you are done.
Section
Part 2
Concept
with open(path, mode) as f: opens the file, runs the indented block, and closes it automatically when the block ends - even if an error is raised inside.
context manager — A block that sets something up and guarantees the cleanup. with open(...) is the standard way to handle files.
Definition probe
Sort into buckets
Every line below is part of the definition of file object or of context manager — one or the other, never both. Put each where it belongs.
Step zero
Discussion prompt
Write inside a with block — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: The with block opens the file as f
Answer:
Worked example
with open("data.txt", "w") as f:
f.write("first line\n")
f.write("second line\n")
print("closed:", f.closed)The with block opens the file as f
Why: Both writes run inside the indented block.
Leaving the block closes f
Why: No f.close() needed - the with statement did it.
Read the output
Why: Verified by execution: prints closed: True. f.closed confirms the file is closed after the block.
| line | action | f.closed |
|---|---|---|
| 1-3 | open + two writes | False |
| after block | auto-close | True |
Error analysis
Annotate
Walk the callouts on Write inside a with block. Each one is a place this is easy to get subtly wrong.
Concept
With a manual f.close(), any error between open and close skips the close and leaks the file. with closes on the way out no matter what - success or crash.
Rule of thumb: always use with open(...). Reach for a bare open only when you have a strong reason.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Manually closing, then writing again by mistake.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: Once f.close() runs, f is dead.
Keep every read and write inside one with block.
Why: Once f.close() runs, f is dead. Writing to it raises ValueError - the extra line never gets saved.
Trap
Manually closing, then writing again by mistake.
f = open("data.txt", "w")
f.write("hi\n")
f.close()
f.write("more\n")Line 4 writes to a closed file
Why: Once f.close() runs, f is dead. Writing to it raises ValueError - the extra line never gets saved.
| line | result |
|---|---|
| f.close() | file closed |
| f.write("more\n") | ValueError: I/O operation on closed file. |
Keep every read and write inside one with block.
with open("data.txt", "w") as f:
f.write("hi\n")
f.write("more\n")Both writes are inside the block
Why: The file stays open for the whole block and closes cleanly at the end. Real output: nothing printed, both lines saved.
| line | f.closed |
|---|---|
| writes | False |
| after block | True |
Section
Part 3
Concept
Open with 'r' (or no mode - it is the default) to read. The file must already exist. You then pull its text out with a read method.
The three ways to read: .read() for the whole thing, .readline() for one line, or a for loop / .readlines() for line by line.
Fill the middle
Fill in the blanks
From .read() returns the whole file — one line has had its right-hand side removed. Put it back.
with open("data.txt", "w") as f:
f.write("first line\nsecond line\n")
with open("data.txt", "r") as f:
text = f.read()
print(text)
Why: text is what everything below it consumes, so the wrong expression here fails later and somewhere else. The \n between them makes two separate lines in the file.
Worked example
with open("data.txt", "w") as f:
f.write("first line\nsecond line\n")
with open("data.txt", "r") as f:
text = f.read()
print(text)First block writes two lines
Why: The \n between them makes two separate lines in the file.
Second block reads it all
Why: f.read() returns every character - including the newlines - as one string.
Read the output
Why: Verified by execution: prints both lines, then a blank line from the final \n.
| call | returns |
|---|---|
| f.read() | 'first line\nsecond line\n' |
| print(text) | first line / second line |
Blank canvas
Draw it
Draw what .read() returns the whole file just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Concept
.readline() returns the next single line, including its trailing \n. Call it again for the line after that - the file remembers where you left off.
Sorting
Sort into buckets
These are the pieces of Session 19 - Files & Text I/O, out of order. Put each one back under the part of the lesson it belongs to.
Step zero
Discussion prompt
Two calls to .readline() — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: First readline gets line one
Answer:
Worked example
with open("data.txt", "w") as f:
f.write("first line\nsecond line\n")
with open("data.txt", "r") as f:
print(f.readline())
print(f.readline())First readline gets line one
Why: Returns "first line\n". print adds its own newline, so you see a blank line after it.
Second readline gets line two
Why: The file advanced, so this returns "second line\n".
Read the output
Why: Verified by execution: first line, blank, second line, blank.
| call | returns |
|---|---|
| 1st readline() | 'first line\n' |
| 2nd readline() | 'second line\n' |
Concept
The cleanest way to process a file: loop directly over it. for line in f: hands you one line per pass, in order, without loading the whole file at once.
Pattern
Predict first
The table runs: 1 | 'apple\n' | apple + blank · 2 | 'banana\n' | banana + blank
In Loop over the lines, given the rows so far: what is the next one — the row where pass is 3?
Correct: 3 | 'cherry\n' | cherry + blank
| pass | line | printed |
|---|---|---|
| 1 | 'apple\n' | apple + blank |
| 2 | 'banana\n' | banana + blank |
| 3 | 'cherry\n' | cherry + blank |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. line is "apple\n", then "banana\n", then "cherry\n".
Worked example
with open("data.txt", "w") as f:
f.write("apple\nbanana\ncherry\n")
with open("data.txt", "r") as f:
for line in f:
print(line)The loop runs once per line
Why: line is "apple\n", then "banana\n", then "cherry\n".
Read the output
Why: Verified by execution: each fruit prints with a blank line under it - line already ends in \n, and print adds another.
| pass | line | printed |
|---|---|---|
| 1 | 'apple\n' | apple + blank |
| 2 | 'banana\n' | banana + blank |
| 3 | 'cherry\n' | cherry + blank |
Pattern
Step through it
Step through Loop over the lines one row at a time. What is driving the change, and what would the row after the last one be?
Concept
.readlines() reads the whole file and returns a list of strings, one per line, each still ending in \n. Handy when you want to index or count lines.
Worked example
with open("data.txt", "w") as f:
f.write("apple\nbanana\ncherry\n")
with open("data.txt", "r") as f:
lines = f.readlines()
print(lines)readlines collects all lines
Why: Each line keeps its trailing \n inside the list.
Read the output
Why: Verified by execution: ['apple\n', 'banana\n', 'cherry\n'] - a real list you can index or loop.
| expression | value |
|---|---|
| len(lines) | 3 |
| lines[0] | 'apple\n' |
| print(lines) | ['apple\n', 'banana\n', 'cherry\n'] |
Section
Part 4
Concept
When you read, every line still carries the \n that ended it in the file. That invisible character is why looped print output looks double-spaced.
Intuition
Think of \n as a pressed Enter key stored in the text. You cannot see it, but it is a character just like a letter.
print then adds its own Enter, so a line read from a file prints as text + its \n + print's \n = a blank gap. .strip() removes the file's \n so the gap disappears.
Worked example
with open("data.txt", "w") as f:
f.write("apple\nbanana\ncherry\n")
with open("data.txt", "r") as f:
for line in f:
print(line.strip())strip() trims whitespace at both ends
Why: That includes the trailing \n, so each line prints tight with no blank gap.
Read the output
Why: Verified by execution: apple, banana, cherry on three lines - no double spacing.
| line | line.strip() | printed |
|---|---|---|
| 'apple\n' | 'apple' | apple |
| 'banana\n' | 'banana' | banana |
| 'cherry\n' | 'cherry' | cherry |
Pattern
Step through it
Step through .strip() removes the trailing newline one row at a time. What is driving the change, and what would the row after the last one be?
Anomaly
Predict first
A student writes this, and it looks reasonable:
Comparing a read line straight against plain text.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The trailing \n makes the strings unequal, so the check prints False even though the word matches.
Strip the line before comparing.
Why: The trailing \n makes the strings unequal, so the check prints False even though the word matches.
Trap
Comparing a read line straight against plain text.
with open("data.txt", "w") as f:
f.write("yes\n")
with open("data.txt", "r") as f:
answer = f.readline()
print(answer == "yes")answer is "yes\n", not "yes"
Why: The trailing \n makes the strings unequal, so the check prints False even though the word matches.
| value | == "yes" |
|---|---|
| 'yes\n' | False |
Strip the line before comparing.
with open("data.txt", "w") as f:
f.write("yes\n")
with open("data.txt", "r") as f:
answer = f.readline().strip()
print(answer == "yes")strip() drops the \n
Why: Now answer is "yes", so the comparison prints True. Real output: True. Strip lines you plan to compare or convert.
| value | == "yes" |
|---|---|
| 'yes' | True |
Concept
int("42\n") actually works because int ignores surrounding whitespace - but stripping first is the safe habit and keeps your intent clear.
For anything picky (exact string matches, splitting), always .strip() a line right after you read it.
Section
Part 5
Concept
Opening in 'w' mode empties the file immediately - before you write a single character. Whatever was there is gone.
Use 'w' when you want a fresh file. Use 'a' when you want to keep the old contents and add to the end.
Ranking
Put in order
Put the moves of 'w' replaces the whole file into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. Opening 'w' again truncates - the original line is erased before "new" is written.
Worked example
with open("data.txt", "w") as f:
f.write("original content\n")
with open("data.txt", "w") as f:
f.write("new\n")
with open("data.txt", "r") as f:
print(f.read())First block writes original content
Why: The file now holds one line.
Second 'w' wipes it, then writes new
Why: Opening 'w' again truncates - the original line is erased before "new" is written.
Read the output
Why: Verified by execution: prints only new. The original content is gone.
| step | file contents |
|---|---|
| after 1st block | original content |
| open 'w' again | (emptied) |
| after 2nd block | new |
Concept
Opening in 'a' keeps the existing text and positions the cursor at the end, so every write adds on. Perfect for logs and running records.
Step zero
Discussion prompt
Append a second line — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: 'w' creates the file with line 1
Answer:
Worked example
with open("log.txt", "w") as f:
f.write("line 1\n")
with open("log.txt", "a") as f:
f.write("line 2\n")
with open("log.txt", "r") as f:
print(f.read())'w' creates the file with line 1
Why: Fresh file, one line.
'a' adds line 2 without erasing
Why: Append mode keeps line 1 and writes line 2 after it.
Read the output
Why: Verified by execution: line 1 then line 2. Both survive.
| step | file contents |
|---|---|
| after 'w' | line 1 |
| after 'a' | line 1 / line 2 |
Comparison
Comparison matrix
From Append a second line: refill the file contents column from what you know. The rest of the table is as it appeared.
| step | file contents |
|---|---|
| after 'w' | line 1 |
| after 'a' | line 1 / line 2 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Meaning to add a line, but opening in 'w'.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: No error, no warning - 'w' just truncates.
Use 'a' to keep what is already there.
Why: No error, no warning - 'w' just truncates. Only day 2 remains; day 1 is lost forever.
Trap
Meaning to add a line, but opening in 'w'.
with open("log.txt", "w") as f:
f.write("day 1\n")
with open("log.txt", "w") as f:
f.write("day 2\n")
with open("log.txt", "r") as f:
print(f.read())The second 'w' wipes day 1
Why: No error, no warning - 'w' just truncates. Only day 2 remains; day 1 is lost forever.
| step | file |
|---|---|
| after 1st 'w' | day 1 |
| after 2nd 'w' | day 2 (day 1 gone) |
Use 'a' to keep what is already there.
with open("log.txt", "w") as f:
f.write("day 1\n")
with open("log.txt", "a") as f:
f.write("day 2\n")
with open("log.txt", "r") as f:
print(f.read())'a' preserves day 1 and adds day 2
Why: Append never truncates. Real output: day 1 then day 2. Reach for 'w' only when you truly want a clean slate.
| step | file |
|---|---|
| after 'w' | day 1 |
| after 'a' | day 1 / day 2 |
Break the constraint
Discussion prompt
The rule this trap just fixed:
Append never truncates. Real output: day 1 then day 2. Reach for 'w' only when you truly want a clean slate.
Now break it on purpose. Build a case that violates it and follow the consequences until something visibly fails. Where does the failure first show up — and would you have noticed it if you had not been looking?
Hint: The dangerous rules are the ones whose violation still produces an answer. If yours fails loudly, try to find one that fails quietly.
Answer:
No error, no warning - 'w' just truncates. Only day 2 remains; day 1 is lost forever.
Concept
f.write(x) needs x to be a string. It also returns the number of characters written, which you can usually ignore.
Passing a number raises a TypeError - convert it first with str(n).
Fill the middle
Fill in the blanks
From write() returns a character count — one line has had its right-hand side removed. Put it back.
with open("data.txt", "w") as f:
n = f.write("hello")
print(n)
Why: n is what everything below it consumes, so the wrong expression here fails later and somewhere else. Verified by execution: prints 5.
Worked example
with open("data.txt", "w") as f:
n = f.write("hello")
print(n)write returns how many chars it wrote
Why: "hello" is 5 characters, so n is 5.
Read the output
Why: Verified by execution: prints 5. Note there is no \n here, so the file has no trailing newline.
| call | returns |
|---|---|
| f.write("hello") | 5 |
| print(n) | 5 |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Passing an int straight to write().
It is wrong. Say what breaks — and say it before you turn the page.
Correct: write only accepts strings, so passing 42 raises a TypeError and nothing is written.
Convert the number to a string first.
Why: write only accepts strings, so passing 42 raises a TypeError and nothing is written.
Trap
Passing an int straight to write().
score = 42
with open("data.txt", "w") as f:
f.write(score)write() rejects the int
Why: write only accepts strings, so passing 42 raises a TypeError and nothing is written.
| call | result |
|---|---|
| f.write(42) | TypeError: write() argument must be str, not int |
Convert the number to a string first.
score = 42
with open("data.txt", "w") as f:
f.write(str(score) + "\n")str(score) makes it writable
Why: str(42) is "42", a valid string. Real output: nothing printed, file holds 42 and a newline.
| call | result |
|---|---|
| f.write(str(42) + "\n") | 3 chars written |
Section
Part 6
Concept
A file object tracks a position - a cursor that starts at the top and moves forward as you read. Once it reaches the end, there is nothing left to read.
Intuition
Reading is like moving a bookmark through a book. Each read advances it. When the bookmark hits the last page, reading again returns nothing.
To reread from the top, you open the file again (a fresh bookmark at page one) - the simplest reset.
Explain it
Discussion prompt
Explain The cursor is a bookmark to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
Reading is like moving a bookmark through a book. Each read advances it. When the bookmark hits the last page, reading again returns nothing.
Hypothesis
Predict first
Reading twice gives empty the second time is about to be worked. State your hypothesis first: which rule or definition decides this one, and what is the first move it forces? Then watch whether the example agrees with you.
Correct: First read() consumes the whole file
Why: The cursor moves to the end and returns all the text.
A hypothesis you wrote down is falsifiable; a vague sense of how it will go is not. If the example opens somewhere else, that gap is the thing worth chasing.
Worked example
with open("data.txt", "w") as f:
f.write("some text\n")
with open("data.txt", "r") as f:
print(repr(f.read()))
print(repr(f.read()))First read() consumes the whole file
Why: The cursor moves to the end and returns all the text.
Second read() finds nothing left
Why: The cursor is already at the end, so read() returns the empty string ''.
Read the output
Why: Verified by execution: 'some text\n' then ''. Same file object, but the second read is empty.
| call | cursor before | returns |
|---|---|---|
| 1st read() | start | 'some text\n' |
| 2nd read() | end | '' |
Anomaly
Predict first
A student writes this, and it looks reasonable:
Reading once to print, then again to count - in the same block.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: f.read() already moved the cursor to the end, so the for loop finds no lines left.
Read once into a variable, then reuse it - or reopen the file.
Why: f.read() already moved the cursor to the end, so the for loop finds no lines left. Only the read() output appears.
Trap
Reading once to print, then again to count - in the same block.
with open("data.txt", "w") as f:
f.write("x\ny\n")
with open("data.txt", "r") as f:
print(f.read())
for line in f:
print("LINE:", line.strip())The loop runs zero times
Why: f.read() already moved the cursor to the end, so the for loop finds no lines left. Only the read() output appears.
| step | cursor | loop passes |
|---|---|---|
| after read() | end | 0 |
Read once into a variable, then reuse it - or reopen the file.
with open("data.txt", "w") as f:
f.write("x\ny\n")
with open("data.txt", "r") as f:
for line in f:
print("LINE:", line.strip())Loop over the file just once
Why: One pass consumes the file exactly once, so every line is seen. Real output: LINE: x then LINE: y.
| step | loop passes |
|---|---|
| single for loop | 2 |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
open takes a path (the file name) and a mode (what you intend to do). The mode is a short string: read, write, or append.; An open file holds a system resource, and writes may sit in a buffer until you close it. Forget to close and your data can be left unsaved or the file stays locked.; Opening a file is like pulling open a drawer: you can now put things in or take things out. But you should close the drawer when you are done.Section
Part 7
Concept
Text is stored as bytes, and the encoding is the rulebook for turning characters into bytes and back. utf-8 handles every character - accents, symbols, emoji.
Pass encoding='utf-8' to open on both write and read. It avoids the platform default biting you when the file has non-English text.
Analogy
Discussion prompt
Explain encoding='utf-8' by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Text is stored as bytes, and the encoding is the rulebook for turning characters into bytes and back. utf-8 handles every character - accents, symbols, emoji.
Fill the middle
Fill in the blanks
From Round-trip an accented word — one line has had its right-hand side removed. Put it back.
with open("data.txt", "w", encoding="utf-8") as f:
f.write("café\n")
with open("data.txt", "r", encoding="utf-8") as f:
text = f.read()
print(text == "café\n")
Why: text is what everything below it consumes, so the wrong expression here fails later and somewhere else. The same encoding on both ends means the characters survive the round trip.
Worked example
with open("data.txt", "w", encoding="utf-8") as f:
f.write("café\n")
with open("data.txt", "r", encoding="utf-8") as f:
text = f.read()
print(text == "café\n")Write with utf-8, read with utf-8
Why: The same encoding on both ends means the characters survive the round trip.
Read the output
Why: Verified by execution: prints True - the string read back exactly equals what was written, accent and all.
| step | value |
|---|---|
| written | 'café\n' |
| read back | 'café\n' |
| are equal | True |
Concept
Open a file that does not exist in 'r' mode and Python raises FileNotFoundError. Reading cannot invent a file that is not there.
'w' and 'a' create the file instead - only read demands it already exist.
Counterexample
Discussion prompt
Open a file that does not exist in 'r' mode and Python raises FileNotFoundError. Reading cannot invent a file that is not there.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Worked example
with open("data.txt", "r") as f:
print(f.read())data.txt does not exist here
Why: There is nothing to read, so open() in 'r' mode fails before the block body runs.
Read the error
Why: Verified by execution: the program stops with the traceback below.
| line | result |
|---|---|
| open("data.txt", "r") | FileNotFoundError: [Errno 2] No such file or directory: 'data.txt' |
Blank canvas
Draw it
Draw what FileNotFoundError on a missing path just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Section
Part 8
Step zero
Discussion prompt
Write a list of names, one per line — before any calculation: what is the plan? Name the moves in order, in plain English, without doing the arithmetic.
Hint: It starts with: Loop and write each name plus a newline
Answer:
Worked example
names = ["Ana", "Ben", "Cara"]
with open("names.txt", "w") as f:
for name in names:
f.write(name + "\n")
with open("names.txt", "r") as f:
print(f.read())Loop and write each name plus a newline
Why: Adding "\n" yourself is what puts each name on its own line.
Read it back to check
Why: Reopen in 'r' and print the whole file.
Read the output
Why: Verified by execution: Ana, Ben, Cara on three lines.
| pass | writes |
|---|---|
| 1 | Ana\n |
| 2 | Ben\n |
| 3 | Cara\n |
Pattern
Step through it
Step through Write a list of names, one per line one row at a time. What is driving the change, and what would the row after the last one be?
Ranking
Put in order
Put the moves of Sum numbers read from a file into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. int(line) turns "90\n" into the number 90 (int ignores the newline).
Worked example
with open("scores.txt", "w") as f:
f.write("90\n80\n100\n")
total = 0
count = 0
with open("scores.txt", "r") as f:
for line in f:
total += int(line)
count += 1
print(total / count)Each line is text until you convert it
Why: int(line) turns "90\n" into the number 90 (int ignores the newline).
Accumulate a running total and count
Why: Standard loop-accumulator, one pass through the file.
Read the output
Why: Verified by execution: 270 / 3 = 90.0.
| line | int(line) | total | count |
|---|---|---|---|
| '90\n' | 90 | 90 | 1 |
| '80\n' | 80 | 170 | 2 |
| '100\n' | 100 | 270 | 3 |
Error analysis
Annotate
Walk the callouts on Sum numbers read from a file. Each one is a place this is easy to get subtly wrong.
Pattern
Predict first
The table runs: 1 | 'a\n' | 1 · 2 | 'b\n' | 2
In Count the lines in a file, given the rows so far: what is the next one — the row where pass is 3?
Correct: 3 | 'c\n' | 3
| pass | line | count |
|---|---|---|
| 1 | 'a\n' | 1 |
| 2 | 'b\n' | 2 |
| 3 | 'c\n' | 3 |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Each pass is one line, so count ends at the number of lines.
Worked example
with open("data.txt", "w") as f:
f.write("a\nb\nc\n")
count = 0
with open("data.txt", "r") as f:
for line in f:
count += 1
print(count)Add one per pass of the loop
Why: Each pass is one line, so count ends at the number of lines.
Read the output
Why: Verified by execution: 3.
| pass | line | count |
|---|---|---|
| 1 | 'a\n' | 1 |
| 2 | 'b\n' | 2 |
| 3 | 'c\n' | 3 |
Pattern
Step through it
Step through Count the lines in a file one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 9
Pattern
1. with open(path, 'r', encoding='utf-8') as f:
Why: with guarantees the file closes; utf-8 keeps every character intact.
2. Loop: for line in f:
Why: Handles the file one line at a time, in order, without loading it all.
3. line = line.strip()
Why: Drop the trailing newline before you compare or convert.
4. Convert if needed: int(line) / float(line)
Why: Lines are always strings; turn them into numbers when you need math.
Pattern
Fresh file? open with 'w'
Why: 'w' creates or truncates - use it when the old contents should go.
Add to the end? open with 'a'
Why: 'a' keeps what is there and writes after it - logs, running records.
Add your own newlines
Why: write() does not add \n; append "\n" to put each item on its own line.
Numbers -> str() first
Why: write() only takes strings, so convert with str(n) before writing.
Real world
Discussion prompt
Outside this lesson: where does Session 19 - Files & Text I/O actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Writing or appending is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 19 of the Python Fundamentals series, in depth. Reading and writing real text files: open(path, mode), the with statement (context manager) that always closes the file, .read()/.readline()/for line in f/.readlines(), writing with 'w' (truncates) versus appending with 'a', stripping the trailing newline, and encoding='utf-8'.
Elimination
Eliminate the wrong options
Which mode keeps the existing contents and adds to the end?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: 'a' is append mode: it keeps the existing text and writes at the end. 'w' would erase it first. Verified by execution.
Check
You want to keep a file's old contents and add to the end.
with open("log.txt", "?") as f:
f.write("new entry\n")| goal | mode |
|---|---|
| keep old text, add at end | ? |
Check your understanding
Which mode keeps the existing contents and adds to the end?
Answer: A
Why: 'a' is append mode: it keeps the existing text and writes at the end. 'w' would erase it first. Verified by execution.
Check
What does the comparison print?
with open("data.txt", "w") as f:
f.write("done\n")
with open("data.txt", "r") as f:
s = f.readline()
print(s == "done")| s | == "done" |
|---|---|
| ? | ? |
Check your understanding
What does this print?
Answer: A
Why: readline() returns "done\n" with the newline, which is not equal to "done", so the comparison is False. Strip first to get True. Verified by execution.
Check
Trace the two reads on one file object.
with open("data.txt", "w") as f:
f.write("hi\n")
with open("data.txt", "r") as f:
a = f.read()
b = f.read()
print(repr(b))| read | returns |
|---|---|
| a | 'hi\n' |
| b | ? |
Check your understanding
What does print(repr(b)) show?
Answer: A
Why: The first read() consumed the whole file and moved the cursor to the end, so the second read() returns the empty string ''. Verified by execution.
Trade off
Comparison matrix
From Check: reading twice: every row here is a choice with a cost. Fill the returns column, then say which row you would actually pick and what you give up for it.
| read | returns |
|---|---|
| a | 'hi\n' |
| b | ? |
Check
nowhere.txt does not exist.
with open("nowhere.txt", "r") as f:
print(f.read())| mode | file missing |
|---|---|
| 'r' | ? |
Check your understanding
What happens?
Answer: A
Why: Opening a nonexistent file in 'r' mode raises FileNotFoundError - read mode never creates a file. Verified by execution.
Check
age holds an int.
age = 30
with open("data.txt", "w") as f:
f.write(age)| argument | type |
|---|---|
| age | int |
Check your understanding
What does f.write(age) do?
Answer: A
Why: write() requires a string; passing the int 30 raises TypeError: write() argument must be str, not int. Convert with str(age) first. Verified by execution.
Check
Trace the loop output.
with open("data.txt", "w") as f:
f.write("a\nb\n")
with open("data.txt", "r") as f:
for line in f:
print(line.strip())| pass | line | prints |
|---|---|---|
| 1 | 'a\n' | ? |
| 2 | 'b\n' | ? |
Check your understanding
What does this print?
Answer: A
Why: The loop yields 'a\n' then 'b\n'; strip() removes each newline, so a and b print on two tight lines. Verified by execution.
Comparison
Comparison matrix
From Check: for line in f: refill the line column from what you know. The rest of the table is as it appeared.
| pass | line | prints |
|---|---|---|
| 1 | 'a\n' | ? |
| 2 | 'b\n' | ? |
Elimination
Eliminate the wrong options
Why is with open(path) as f: preferred over a bare open + close?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: with is a context manager: it guarantees the file is closed when the block ends, even on an exception, so writes are flushed and resources freed. Verified by reasoning and execution of f.closed.
Check
What is the main reason to prefer with open(...)?
Check your understanding
Why is with open(path) as f: preferred over a bare open + close?
Answer: A
Why: with is a context manager: it guarantees the file is closed when the block ends, even on an exception, so writes are flushed and resources freed. Verified by reasoning and execution of f.closed.
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Opening a File · The with Statement · Reading a File · Newlines & .strip() · Writing: 'w' vs 'a' · The Read Cursor. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
You can open, read, and write text files - and let with close them for you no matter what.
| You write | It means |
|---|---|
| with open(p, 'r') as f: | open to read; auto-close at block end |
| f.read() / f.readline() | whole file / one line (keeps \n) |
| for line in f: | loop line by line |
| line.strip() | drop the trailing newline |
| open(p, 'w') | write - ERASES the file first |
| open(p, 'a') | append - keeps old text, adds at end |
Remember the traps: 'w' silently erases, reading twice gives '', a missing file in 'r' raises FileNotFoundError, and write() takes only strings. Next session builds on this to work with structured data.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.