Session 19 - Files & Text I/O

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

What this lesson covers

The lesson, slide by slide

1. Files & Text I/O

Title

Python Fundamentals - Session 19

Read and write real files on disk - and close them safely

2. What you will be able to do

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:

  1. Open a file with open(path, mode) and choose the right mode.
  2. Read a file with .read(), .readline(), a for loop, or .readlines().
  3. Write with 'w' and append with 'a' - and know which one erases.
  1. Use with open(...) as f: so the file always closes.
  2. Strip the trailing newline and set encoding='utf-8'.
  3. Recognize FileNotFoundError and the read-twice-gives-empty trap.

3. What survived from Session 18 - Mutability & Aliasing?

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.

4. Opening a File

Section

Part 1

5. A file is text stored on disk

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.

6. Break it if you can: A file is text stored on disk

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.

7. open(path, mode) picks the job

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.

modemeansif file is missingexisting content
'r'read (default)FileNotFoundErrorkept
'w'writecreates itERASED
'a'appendcreates itkept, adds at end

8. Fill in: means for open(path, mode) picks the job

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.

modemeansif file is missingexisting content
'r'read (default)FileNotFoundErrorkept
'w'writecreates itERASED
'a'appendcreates itkept, adds at end

9. What has to happen first: Open, write one line, close

Ranking

Put in order

Put the moves of Open, write one line, close into the order they have to happen.

  1. Line 1 opens data.txt for writing
  2. Line 2 writes the text
  3. Read the output

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.

10. Open, write one line, close

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.

stepresult
open(..., 'w')file object f
f.write("hello\n")5 chars written
f.close()saved to disk
printdone

11. What each one costs: Open, write one line, close

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.

stepresult
open(..., 'w')file object f
f.write("hello\n")5 chars written
f.close()saved to disk
printdone

12. Closing matters

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.

13. By analogy: Closing matters

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.

14. Opening a file is like opening a drawer

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.

15. Teach it back: Opening a file is like opening a drawer

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.

16. The with Statement

Section

Part 2

17. with closes the file for you

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.

18. Take the definitions apart: file object vs context manager

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.

file object
The value open() hands back.; You call methods on it (.read(), .write()) and then close it.
context manager
A block that sets something up and guarantees the cleanup.; with open(...) is the standard way to handle files.
b1
The value open() hands back. You call methods on it (.read(), .write()) and then close it.
b2
A block that sets something up and guarantees the cleanup. with open(...) is the standard way to handle files.

19. Plan first: Write inside a with block

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:

  1. The with block opens the file as f
  2. Leaving the block closes f
  3. Read the output

20. Write inside a with block

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.

lineactionf.closed
1-3open + two writesFalse
after blockauto-closeTrue

21. Inspect it line by line: Write inside a with block

Error analysis

Annotate

Walk the callouts on Write inside a with block. Each one is a place this is easy to get subtly wrong.

  • Both writes run inside the indented block.
  • No f.close() needed - the with statement did it.
  • Verified by execution: prints closed: True. f.closed confirms the file is closed after the block.

22. Why with is safer than manual close

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.

23. Something is wrong here: using a file after it is closed

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.

24. Trap: using a file after it is closed

Trap

The 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.

lineresult
f.close()file closed
f.write("more\n")ValueError: I/O operation on closed file.

The fix

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.

linef.closed
writesFalse
after blockTrue

25. Reading a File

Section

Part 3

26. Mode 'r' reads

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.

27. Restore the missing line: .read() returns the whole file

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.

28. .read() returns the whole 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.

callreturns
f.read()'first line\nsecond line\n'
print(text)first line / second line

29. Draw the shape of it: .read() returns the whole file

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.

30. .readline() reads one line

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.

31. Where does each piece belong: Session 19 - Files & Text I/O

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.

Opening a File
A file is text stored on disk; open(path, mode) picks the job; Open, write one line, close
The with Statement
with closes the file for you; Write inside a with block; Why with is safer than manual close
Reading a File
Mode 'r' reads; .read() returns the whole file; .readline() reads one line
s1
Opening a File is where Session 19 - Files & Text I/O puts A file is text stored on disk, open(path, mode) picks the job, Open, write one line, close. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
The with Statement is where Session 19 - Files & Text I/O puts with closes the file for you, Write inside a with block, Why with is safer than manual close. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Reading a File is where Session 19 - Files & Text I/O puts Mode 'r' reads, .read() returns the whole file, .readline() reads one line. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

32. Plan first: Two calls to .readline()

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:

  1. First readline gets line one
  2. Second readline gets line two
  3. Read the output

33. Two calls to .readline()

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.

callreturns
1st readline()'first line\n'
2nd readline()'second line\n'

34. for line in f reads line by line

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.

35. Predict the next row: Loop over the lines

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

passlineprinted
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".

36. Loop over the lines

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.

passlineprinted
1'apple\n'apple + blank
2'banana\n'banana + blank
3'cherry\n'cherry + blank

37. Watch it run: Loop over the lines

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?

  1. Step 1: pass is 1
  2. Step 2: pass is 2
  3. Step 3: pass is 3

38. .readlines() gives a list

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.

39. Read every line into a list

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.

expressionvalue
len(lines)3
lines[0]'apple\n'
print(lines)['apple\n', 'banana\n', 'cherry\n']

40. Newlines & .strip()

Section

Part 4

41. Each line keeps its newline

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.

42. The newline is real, just invisible

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.

43. .strip() removes the trailing newline

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.

lineline.strip()printed
'apple\n''apple'apple
'banana\n''banana'banana
'cherry\n''cherry'cherry

44. Watch it run: .strip() removes the trailing newline

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?

  1. Step 1: line is 'apple\n'
  2. Step 2: line is 'banana\n'
  3. Step 3: line is 'cherry\n'

45. Something is wrong here: forgetting to strip the newline

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.

46. Trap: forgetting to strip the newline

Trap

The 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

The fix

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

47. Strip before converting, too

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.

48. Writing: 'w' vs 'a'

Section

Part 5

49. 'w' truncates first

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.

50. What has to happen first: 'w' replaces the whole file

Ranking

Put in order

Put the moves of 'w' replaces the whole file into the order they have to happen.

  1. First block writes original content
  2. Second 'w' wipes it, then writes new
  3. Read the output

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.

51. 'w' replaces the whole file

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.

stepfile contents
after 1st blockoriginal content
open 'w' again(emptied)
after 2nd blocknew

52. 'a' appends to the end

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.

53. Plan first: Append a second line

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:

  1. 'w' creates the file with line 1
  2. 'a' adds line 2 without erasing
  3. Read the output

54. Append a second line

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.

stepfile contents
after 'w'line 1
after 'a'line 1 / line 2

55. Fill in: file contents for Append a second line

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.

stepfile contents
after 'w'line 1
after 'a'line 1 / line 2

56. Something is wrong here: 'w' silently erases the file

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.

57. Trap: 'w' silently erases the file

Trap

The 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.

stepfile
after 1st 'w'day 1
after 2nd 'w'day 2 (day 1 gone)

The fix

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.

stepfile
after 'w'day 1
after 'a'day 1 / day 2

58. Break it on purpose: 'w' silently erases the file

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.

59. write() takes strings only

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).

60. Restore the missing line: write() returns a character count

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.

61. write() returns a character count

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.

callreturns
f.write("hello")5
print(n)5

62. Something is wrong here: writing a number instead of a string

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.

63. Trap: writing a number instead of a string

Trap

The 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.

callresult
f.write(42)TypeError: write() argument must be str, not int

The fix

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.

callresult
f.write(str(42) + "\n")3 chars written

64. The Read Cursor

Section

Part 6

65. Reading moves a cursor

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.

66. The cursor is a bookmark

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.

67. Teach it back: The cursor is a bookmark

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.

68. State the rule before it runs: Reading twice gives empty the second time

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.

69. Reading twice gives empty the second time

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.

callcursor beforereturns
1st read()start'some text\n'
2nd read()end''

70. Something is wrong here: reading the same file object twice

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.

71. Trap: reading the same file object twice

Trap

The 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.

stepcursorloop passes
after read()end0

The fix

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.

steploop passes
single for loop2

72. Which of these survive contact with Session 19 - Files & Text I/O?

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.

Holds up
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.
Breaks
Manually closing, then writing again by mistake.; Comparing a read line straight against plain text.
sound
These are stated as this lesson states them — each one survives the edge cases Session 19 - Files & Text I/O puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

73. Encoding & Missing Files

Section

Part 7

74. encoding='utf-8'

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.

75. By analogy: encoding='utf-8'

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.

76. Restore the missing line: Round-trip an accented word

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.

77. Round-trip an accented word

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.

stepvalue
written'café\n'
read back'café\n'
are equalTrue

78. 'r' on a missing file raises

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.

79. Break it if you can: 'r' on a missing file raises

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.

80. FileNotFoundError on a missing path

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.

lineresult
open("data.txt", "r")FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'

81. Draw the shape of it: FileNotFoundError on a missing path

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.

82. Putting It Together

Section

Part 8

83. Plan first: Write a list of names, one per line

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:

  1. Loop and write each name plus a newline
  2. Read it back to check
  3. Read the output

84. Write a list of names, one per line

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.

passwrites
1Ana\n
2Ben\n
3Cara\n

85. Watch it run: Write a list of names, one per line

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?

  1. Step 1: pass is 1
  2. Step 2: pass is 2
  3. Step 3: pass is 3

86. What has to happen first: Sum numbers read from a file

Ranking

Put in order

Put the moves of Sum numbers read from a file into the order they have to happen.

  1. Each line is text until you convert it
  2. Accumulate a running total and count
  3. Read the output

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).

87. Sum numbers read from a file

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.

lineint(line)totalcount
'90\n'90901
'80\n'801702
'100\n'1002703

88. Inspect it line by line: Sum numbers read from a file

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.

  • int(line) turns "90\n" into the number 90 (int ignores the newline).
  • Standard loop-accumulator, one pass through the file.

89. Predict the next row: Count the lines in a file

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

passlinecount
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.

90. Count the lines in a file

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.

passlinecount
1'a\n'1
2'b\n'2
3'c\n'3

91. Watch it run: Count the lines in a file

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?

  1. Step 1: pass is 1
  2. Step 2: pass is 2
  3. Step 3: pass is 3

92. Patterns & Checks

Section

Part 9

93. Reading a file safely

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.

94. Writing or appending

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.

95. Where this shows up: Session 19 - Files & Text I/O

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'.

96. Rule out three: Check: which mode erases?

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.

  • A. 'a'
  • B. 'w'
  • C. 'r'
  • D. 'x'

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.

97. Check: which mode erases?

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")
goalmode
keep old text, add at end?

Check your understanding

Which mode keeps the existing contents and adds to the end?

  • A. 'a' (correct)
  • B. 'w'
  • C. 'r'
  • D. 'x'

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.

Why B tempts people
'w' truncates the file to empty before writing, so the old contents are lost.
Why C tempts people
'r' is read-only - you cannot write with it at all.
Why D tempts people
'x' creates a new file but fails if it already exists; it does not append.

98. Check: the trailing newline

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?

  • A. False (correct)
  • B. True
  • C. done
  • D. Error

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.

Why B tempts people
s still holds the trailing \n, so "done\n" != "done" - it is False, not True.
Why C tempts people
The code prints the boolean result of ==, not the string s.
Why D tempts people
Comparing two strings is always valid - no error is raised.

99. Check: reading twice

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))
readreturns
a'hi\n'
b?

Check your understanding

What does print(repr(b)) show?

  • A. '' (correct)
  • B. 'hi\n'
  • C. 'hi'
  • D. None

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.

Why B tempts people
The cursor is already at the end after a, so b does not re-read the text.
Why C tempts people
read() never strips the newline; and here b is empty regardless.
Why D tempts people
read() at end-of-file returns '' (an empty string), not None.

100. What each one costs: Check: reading twice

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.

readreturns
a'hi\n'
b?

101. Check: missing file

Check

nowhere.txt does not exist.

with open("nowhere.txt", "r") as f:
    print(f.read())
modefile missing
'r'?

Check your understanding

What happens?

  • A. FileNotFoundError (correct)
  • B. It creates an empty nowhere.txt
  • C. It prints an empty string
  • D. It prints None

Answer: A

Why: Opening a nonexistent file in 'r' mode raises FileNotFoundError - read mode never creates a file. Verified by execution.

Why B tempts people
'w' and 'a' create a missing file, but 'r' does not - it raises instead.
Why C tempts people
The error happens at open(), before any read() could return a string.
Why D tempts people
No value is returned at all; the program stops with a traceback.

102. Check: writing a number

Check

age holds an int.

age = 30
with open("data.txt", "w") as f:
    f.write(age)
argumenttype
ageint

Check your understanding

What does f.write(age) do?

  • A. Raises TypeError (correct)
  • B. Writes 30 to the file
  • C. Writes '30' to the file
  • D. Writes nothing but succeeds

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.

Why B tempts people
write() does not auto-convert numbers - it rejects the int with a TypeError.
Why C tempts people
You would get '30' only if you wrote str(age); write() will not convert for you.
Why D tempts people
It does not silently succeed - it raises before writing anything.

103. Check: for line in f

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())
passlineprints
1'a\n'?
2'b\n'?

Check your understanding

What does this print?

  • A. a then b on two lines (correct)
  • B. a and b with a blank line between them
  • C. ['a\n', 'b\n']
  • D. ab on one line

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.

Why B tempts people
strip() removes the trailing \n, so there is no extra blank line - that happens only without strip().
Why C tempts people
A for loop prints each line separately; it does not show a list. That would be readlines().
Why D tempts people
Each print call ends its own line, so a and b land on separate lines, not joined.

104. Fill in: line for Check: for line in f

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.

passlineprints
1'a\n'?
2'b\n'?

105. Rule out three: Check: why use with?

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.

  • A. It closes the file automatically, even if an error is raised
  • B. It makes reading faster
  • C. It lets you skip choosing a mode
  • D. It reads the whole file into memory at once

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.

106. Check: why use with?

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?

  • A. It closes the file automatically, even if an error is raised (correct)
  • B. It makes reading faster
  • C. It lets you skip choosing a mode
  • D. It reads the whole file into memory at once

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.

Why B tempts people
with does not change read speed - it manages opening and closing, not performance.
Why C tempts people
You still pass a mode to open(); with does not choose it for you.
Why D tempts people
with does not read anything by itself - you still call .read() or loop over f.

107. Connect it up: Session 19 - Files & Text I/O

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.

108. What you can do now

Recap

You can open, read, and write text files - and let with close them for you no matter what.

You writeIt 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.

Sources

  1. Python 3 Tutorial - Reading and Writing Files
  2. Python 3 - open() built-in function
  3. Python 3 - io module (text I/O)
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 19).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108