Learning Goals
3 minBy the end of this lesson you can:
- Open a text file with
open("name.txt")wrapped in awithblock — the modern, safe pattern. - Choose between
.read()(everything as one string),.readlines()(a list of lines) andfor line in f:(one line at a time). - Strip the trailing newline from each line with
.rstrip("\\n"). - Handle a missing file with
FileNotFoundError— a sneak peek at PY-L2-26's try/except.
Setup · A Tiny Text File
5 minMake a folder for this lesson. Inside it, create both files side by side:
read_files.py— the Python file you'll write today.quotes.txt— a small text file. Paste these five lines into it (use plain notepad / VS Code, not a Word document — Word files aren't plain text):
Code is read more often than it is written. Premature optimisation is the root of all evil. There should be one obvious way to do it. Simple is better than complex. Now is better than never.
Python looks for quotes.txt relative to where you run the script, not where the script is saved. If your code can't find the file, check that you're running the script from the same folder — in VS Code, that's usually automatic; on the command line, cd there first.
New Concept · open(), with, and the Three Reads
14 minThe with open(...) pattern
The modern Python idiom for opening a file is:
with open("quotes.txt") as f: text = f.read() print(text)
Three pieces:
open("quotes.txt")opens the file. Default mode is read text.with ... as f:binds the file to the nameffor the indented block.- When the block ends, the file is closed automatically — even if something crashes inside. That's the "safe" part.
You'll see older code with f = open(...) and a separate f.close(). That style is brittle: forget the close, leak the file. The with pattern can't forget.
Three ways to actually read
1 · .read() — everything as one big string.
with open("quotes.txt") as f: text = f.read() print(text) print("Type:", type(text)) # → <class 'str'> print("Chars:", len(text)) # one count for the whole file
Good for short files. Avoid on huge files — it loads everything into RAM.
2 · .readlines() — a list of lines.
with open("quotes.txt") as f: lines = f.readlines() print(lines) # → ['Code is read more often than it is written.\n', 'Premature ...\n', ...] print("How many lines:", len(lines)) print("First line :", lines[0])
Notice the \\n at the end of each line — the newline character that separates lines on disk is kept. Almost always you'll want to strip it; we'll see how in a moment.
3 · for line in f: — one line at a time.
with open("quotes.txt") as f: for line in f: print(line, end="") # end="" because the line already ends with \n
This is the modern favourite. It's memory-efficient (only one line in RAM at a time) and reads beautifully. The end="" on print stops Python adding a second newline — the line itself already has one.
Stripping the newline
Use .rstrip("\\n") to trim the trailing newline from each line — same family as the .strip() from PY-L2-14.
with open("quotes.txt") as f: for line in f: clean = line.rstrip("\n") print(f"-> {clean}")
A naked .strip() also works (it trims any whitespace on both ends), but .rstrip("\\n") is more explicit about what you're removing.
The decision table
Goal Pick
Read entire file as one string f.read()
Get a list of lines (with newlines) f.readlines()
Walk lines one by one (the favourite) for line in f:
Walk and turn into a clean list [line.rstrip("\n") for line in f]Encoding — say it once and forget
For most modern text — Roman alphabet, emojis, Malay characters — pass encoding="utf-8" when you open. It's a tiny habit that prevents weird crashes on non-English text:
with open("quotes.txt", encoding="utf-8") as f: text = f.read()
If you're on Windows and ever get a UnicodeDecodeError, encoding is usually the fix.
When the file isn't there
Open a name that doesn't exist and Python crashes with FileNotFoundError. You can catch it cleanly with try / except — a topic we'll do properly in PY-L2-26, but the shape is worth seeing now:
try: with open("missing.txt") as f: print(f.read()) except FileNotFoundError: print("That file isn't here.")
Worked Example · The Quote-of-the-Day Reader
12 minThe story
Build a tiny program that loads quotes.txt, prints all five quotes numbered, and then picks one at random as "today's quote".
Save as read_files.py:
Code
# read_files.py — load, list, and pick a quote import random with open("quotes.txt", encoding="utf-8") as f: quotes = [line.rstrip("\n") for line in f] print(f"Loaded {len(quotes)} quotes.\n") for i, q in enumerate(quotes, start=1): print(f"{i}. {q}") print() print("=== Quote of the day ===") print(random.choice(quotes))
Sample output (the last line will vary)
Loaded 5 quotes. 1. Code is read more often than it is written. 2. Premature optimisation is the root of all evil. 3. There should be one obvious way to do it. 4. Simple is better than complex. 5. Now is better than never. === Quote of the day === Simple is better than complex.
Read the diff
Two patterns to spot. First, the "clean list" trick: [line.rstrip("\\n") for line in f] reads and tidies in one line — a list comprehension over the open file. Second, the file is closed automatically the moment we leave the with block, so the rest of the program (the printing, the random pick) operates on the in-memory list — never on the open file. That's the right shape: open, slurp, close, then work.
For huge files (megabytes+), don't do [line.rstrip() for line in f] — that loads the whole thing into RAM as a list. Loop with for line in f: and process each line as it comes. You'll see why properly when we hit the Wordle word bank in PY-L2-23 — even ~10k lines is fine to slurp, but the habit matters for the day you read a real log file.
Try It Yourself
13 minAll exercises use quotes.txt from the setup section.
Read the file as a clean list. Print the longest quote and how many characters it has.
Hint
with open("quotes.txt", encoding="utf-8") as f: quotes = [line.rstrip("\n") for line in f] longest = "" for q in quotes: if len(q) > len(longest): longest = q print(longest) print(f"({len(longest)} characters)")
This is the "biggest in a list" scan from PY-L1-22 — except the list came from disk instead of being hard-coded.
Print the total number of words across all quotes, using .split() on each line.
Hint
with open("quotes.txt", encoding="utf-8") as f: total = 0 for line in f: words = line.split() total += len(words) print(f"Total words: {total}")
Notice the running-total pattern inside the file loop. No need to build an intermediate list — we count as we go.
Read the file. Print only the quotes that contain the word better. Use in for the membership check.
Hint
with open("quotes.txt", encoding="utf-8") as f: for line in f: clean = line.rstrip("\n") if "better" in clean.lower(): print(clean)
The .lower() makes the search case-insensitive — handy when the file might mix cases. Writing back to the file is tomorrow's lesson.
Mini-Challenge · Read a Bigger File
8 minSave a new file names.txt with at least 20 names — one per line. Mix the case and add stray spaces around some of them to make it realistic:
Aisyah Wei Jie Priya iman AIZAT Hafiz ... (twenty in total)
Build read_names.py that:
- Reads the file with
for line in f:. - Cleans each line with
.strip()and skips empty lines. - Builds a list of names in title-case (
.title()— first letter of every word in upper case). - Prints the list sorted alphabetically.
- Prints the total count.
- Picks 3 random names with
random.samplefor "today's presenters".
Stretch goal. Print only the names starting with a vowel — use name[0] in "AEIOU".
Show one possible solution
# read_names.py — read, clean, sort, sample import random names = [] with open("names.txt", encoding="utf-8") as f: for line in f: clean = line.strip() if clean: # skip empties names.append(clean.title()) names.sort() print(f"Loaded {len(names)} names:") for n in names: print(" -", n) print() print("Today's presenters:", random.sample(names, 3)) # Stretch — vowel names vowel_names = [n for n in names if n[0] in "AEIOU"] print("Names starting with a vowel:", vowel_names)
Non-negotiables: a with open block, a for line in f: loop, .strip() + skip-empty check, .title(), a random.sample pick, and a sorted print. This is the standard shape for "ingest a text file".
Recap
3 minRead a file safely with with open(name, encoding="utf-8") as f:. Three flavours: .read() for everything as one string, .readlines() for a list of newline-terminated lines, and the modern favourite for line in f: for one line at a time. Strip the trailing \\n with .rstrip("\\n"). Catch a missing file with except FileNotFoundError:. Slurp into a list with a comprehension when the file is small; loop line-by-line when it's big.
Vocabulary Card
- open(name, mode)
- Open a file. Default mode is
"r"— read text. - with ... as f:
- Context manager — guarantees the file is closed when the block ends.
- .read() / .readlines() / for line in f
- Three ways to actually pull the bytes off disk.
- \\n
- The newline character. Lines from a file usually end with it;
.rstrip("\\n")trims it. - encoding="utf-8"
- The encoding name that handles modern text including emojis and Malay characters.
Homework
4 minSave a new text file shopping.txt with at least 10 lines, each of this form (no commas — split on spaces; the last token is the price):
apple 4 1.20 bread 1 3.50 milk 2 5.40 egg 12 0.50 rice 1 22.00 ...
Build parse_shopping.py that:
- Reads the file with
for line in f:. - For each line,
.split()into three pieces — name, qty, price — and convert qty tointand price tofloat. - Build a list of dicts:
{"name": ..., "qty": ..., "price": ..., "line_total": qty * price}— the pattern from PY-L2-11. - Print every item with f-strings + width specifiers from PY-L2-16. Right-align prices.
- Print the grand total at the bottom.
Stretch. Catch the case where a line has the wrong number of pieces — print a warning and skip that line.
Sample · parse_shopping.py
# parse_shopping.py — read, parse, total items = [] with open("shopping.txt", encoding="utf-8") as f: for line_no, line in enumerate(f, start=1): parts = line.split() if len(parts) != 3: print(f" ! Skipped line {line_no}: {line.rstrip()}") continue name, qty_s, price_s = parts qty = int(qty_s) price = float(price_s) items.append({"name": name, "qty": qty, "price": price, "line_total": qty * price}) print(f"{'Item':<10}{'Qty':>5}{'Price':>9}{'Total':>9}") print("-" * 33) total = 0 for it in items: total += it["line_total"] print(f"{it['name']:<10}{it['qty']:>5}{it['price']:>9.2f}{it['line_total']:>9.2f}") print("-" * 33) print(f"{'GRAND TOTAL':<23}{total:>10.2f}")
Non-negotiables: a with open read, a .split() into three parts with type conversion, a list of dicts in the shape of PY-L2-11, and a polished printout using PY-L2-16 specifiers. The Stretch test for "wrong number of parts" is your first real defensive-programming move.