File I/O Deep Dive
Layer 7 begins. We go deeper on file handling than Session 29's introduction โ proper resource management, different file modes, and safely reading large files.
Guaranteed cleanup, even when something goes wrong mid-read.
1. Learning Objective
By the end of this session you will be able to:
- Explain why the with statement is the correct way to open a file, versus manual open()/close()
- Use different file modes: read, write, append
- Read a file line by line instead of loading it all into memory at once
- Handle a file encoding issue gracefully
- Refactor CountryRepository to build itself from a real file path, cleanly, using everything from this session
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
Why is with open(path) as f: ... preferred over manually calling f = open(path) and f.close() afterward?
with guarantees cleanup (closing the file) happens even if an exception is raised partway through โ a manual f.close() placed after risky code would be skipped entirely if an exception occurred first.What is the difference between opening a file with mode "w" versus mode "a"?
Why would you read a very large file line by line (with `for line in f:`) instead of `f.read()` all at once?
f.read() could crash the program or the machine. Iterating line by line processes the file incrementally, using a small, constant amount of memory regardless of the file's total size.A file contains text that is not valid UTF-8 (the default assumed encoding). What happens when you try to open() and read it without specifying an encoding, and how would you handle this per Session 11?
UnicodeDecodeError โ another case for Session 11's try/except, or for specifying the correct encoding explicitly if it is known.How does refactoring CountryRepository to accept a file_path and build itself from it (using with, proper modes, and encoding awareness) relate to Session 28's original design?
3. The Concept โ Robust File Handling
"w" replaces everything; "a" preserves existing content and adds to the end.
with โ guaranteed cleanup, even on error
The with statement (a "context manager") guarantees a file is properly closed when the block ends, whether it ends normally or because of an exception โ directly connecting to Session 11's finally block concept, but automated and less error-prone.
# Risky โ if something raises an exception between open() and close(), the file leaks open
f = open("countries.json")
data = f.read()
f.close() # this line is skipped entirely if the read() line raised an exception!
# Safe โ the file is guaranteed to close, no matter what happens inside the block
with open("countries.json") as f:
data = f.read()
# f is already closed here, even if an exception occurred inside the block
File modes
The second argument to open() controls how the file is accessed: "r" (read, default), "w" (write, overwrites), "a" (append, preserves existing content).
# Read (default) โ file must already exist
with open("countries.json", "r") as f:
content = f.read()
# Write โ creates the file if missing, OVERWRITES if it exists
with open("log.txt", "w") as f:
f.write("First line\n")
# Append โ creates the file if missing, adds to the END if it exists
with open("log.txt", "a") as f:
f.write("Another line, added without erasing what was there\n")
Reading large files incrementally
Iterating a file object directly reads it one line at a time, using a small, constant amount of memory โ essential for files too large to comfortably fit in memory all at once.
# Loads the ENTIRE file into memory at once โ risky for very large files
with open("huge_log.txt") as f:
content = f.read()
# Processes one line at a time โ memory usage stays small regardless of file size
with open("huge_log.txt") as f:
for line in f:
if "ERROR" in line:
print(line.strip())
Handling encoding issues
Text files can be encoded differently. Specifying the encoding explicitly, or handling a decode error gracefully, prevents a crash on unexpected file content.
try:
with open("countries.json", encoding="utf-8") as f:
content = f.read()
except UnicodeDecodeError as e:
print(f"File is not valid UTF-8: {e}")
content = None
4. Lab
What you will build
A file called file_io_lab.py, extending the country_explorer package.
Step-by-step instructions
Create the file and write a robust load_json_file function
# file_io_lab.py
import json
def load_json_file(path):
try:
with open(path, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
print(f"{path} does not exist โ using an empty dataset")
return []
except json.JSONDecodeError as e:
print(f"{path} contains invalid JSON: {e}")
return []
except UnicodeDecodeError as e:
print(f"{path} is not valid UTF-8: {e}")
return []
Add an append-mode logging function using mode "a"
def log_load_attempt(path, success):
with open("load_log.txt", "a") as f:
status = "SUCCESS" if success else "FAILED"
f.write(f"{status}: {path}\n")
Combine both into a robust repository-building function
from country_explorer import CountryRepository
def build_repository_from_file(path):
data = load_json_file(path)
log_load_attempt(path, success=bool(data))
return CountryRepository(raw_data=data)
repo = build_repository_from_file("countries.json")
print(len(repo.get_all()))
Test the failure paths explicitly
Try a missing file and confirm both the graceful fallback and the log entry.
missing_repo = build_repository_from_file("does_not_exist.json")
print(len(missing_repo.get_all())) # 0 โ graceful, no crash
Read load_log.txt line by line and print only FAILED entries
Practice the line-by-line iteration pattern on the log file you just created.
with open("load_log.txt") as f:
for line in f:
if line.startswith("FAILED"):
print(line.strip())
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
file_io_lab.py |
Created | Robust file loading with with, modes, encoding handling, and repository construction. |
load_log.txt |
Generated | An append-only log of file load attempts. |
docs/sessions/session-41/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add file_io_lab.py load_log.txt docs/sessions/session-41/index.html
git commit -m "session-41: robust file I/O with proper resource management and encoding handling"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
Why is with open(path) as f: ... preferred over manually calling f = open(path) and f.close() afterward?
with guarantees cleanup (closing the file) happens even if an exception is raised partway through โ a manual f.close() placed after risky code would be skipped entirely if an exception occurred first.What is the difference between opening a file with mode "w" versus mode "a"?
Why would you read a very large file line by line (with `for line in f:`) instead of `f.read()` all at once?
f.read() could crash the program or the machine. Iterating line by line processes the file incrementally, using a small, constant amount of memory regardless of the file's total size.A file contains text that is not valid UTF-8 (the default assumed encoding). What happens when you try to open() and read it without specifying an encoding, and how would you handle this per Session 11?
UnicodeDecodeError โ another case for Session 11's try/except, or for specifying the correct encoding explicitly if it is known.How does refactoring CountryRepository to accept a file_path and build itself from it (using with, proper modes, and encoding awareness) relate to Session 28's original design?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why would using mode "w" instead of "a" for log_load_attempt be a serious bug? What would happen to the log over multiple runs?
- What would happen if build_repository_from_file used f.read() to load a truly enormous countries.json instead of relying on json.load's own internal handling? Is this a realistic concern for this specific project?
- How does this session's with statement connect back to Session 11's finally block โ are they solving a similar problem in a different way?
- Why did CountryRepository itself need zero changes for this session's new, more robust file-loading logic to work?
10. What Breaks If This Knowledge Is Missing?
- Leaked file handles: Without with, an exception between opening and closing a file leaves it open indefinitely โ in a long-running program handling many files, this can exhaust the operating system's limit on open file handles, causing mysterious failures far from the actual bug.
- Overwritten logs: Using mode "w" instead of "a" for an ongoing log file would silently erase all previous history every time the program restarts โ a data-loss bug that is easy to make and often goes unnoticed until the history is actually needed.
- Real API responses (Session 42): The next session introduces genuinely unpredictable external data over the network โ the same resource-management and error-handling discipline from this session (with, specific exception handling) applies directly to handling an HTTP connection safely.
11. What We Learned
Python concept mastered: Robust file I/O โ the with statement for guaranteed cleanup, file modes, line-by-line reading for large files, and encoding-aware error handling.
Unlocks: The project can now safely and robustly read from real files on disk, handling every realistic failure mode gracefully โ the foundation for the real API work in the next session.
Next session: Session 42 โ Calling a Real API with requests. We connect to a real, live network API for the first time โ the REST Countries API โ replacing our JSON file with genuinely external, unpredictable data.