Layer 7 Session 41 Real World

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.

"name": "Kenya" countries.json

Guaranteed cleanup, even when something goes wrong mid-read.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

Why is with open(path) as f: ... preferred over manually calling f = open(path) and f.close() afterward?

This is a specific application of Session 11's error-handling philosophy: 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.
Question 2 of 5

What is the difference between opening a file with mode "w" versus mode "a"?

"w" truncates and overwrites โ€” dangerous if you meant to keep existing content. "a" appends to the end of existing content without touching what came before, useful for something like an ongoing log file.
Question 3 of 5

Why would you read a very large file line by line (with `for line in f:`) instead of `f.read()` all at once?

For files far larger than available memory, loading the whole thing with 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.
Question 4 of 5

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?

Text files can be encoded in different ways (UTF-8, Latin-1, etc.). Reading with the wrong assumed encoding raises UnicodeDecodeError โ€” another case for Session 11's try/except, or for specifying the correct encoding explicitly if it is known.
Question 5 of 5

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?

This is Session 28's payoff, delivered a third time (after the mock data and the simple JSON file in Session 29): only the data SOURCE changes; get_all() and find_by_region() need zero modification, because they were never coupled to how raw_data was originally obtained.

3. The Concept โ€” Robust File Handling

MODE "W"old contentreplacedMODE "A"old content kept,new appended

"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

Lab objective: Rebuild CountryRepository to read from a real file path using with, correct modes, and encoding-aware error handling, with zero changes to its existing methods.

What you will build

A file called file_io_lab.py, extending the country_explorer package.

Step-by-step instructions

1

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 []
2

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")
3

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()))
4

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
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 3 files.

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"
Do not commit until you can answer out loud: "Why does with open(...) as f: guarantee the file closes even if json.load(f) raises an exception inside the block?"

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.

Question 1 of 5

Why is with open(path) as f: ... preferred over manually calling f = open(path) and f.close() afterward?

This is a specific application of Session 11's error-handling philosophy: 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.
Question 2 of 5

What is the difference between opening a file with mode "w" versus mode "a"?

"w" truncates and overwrites โ€” dangerous if you meant to keep existing content. "a" appends to the end of existing content without touching what came before, useful for something like an ongoing log file.
Question 3 of 5

Why would you read a very large file line by line (with `for line in f:`) instead of `f.read()` all at once?

For files far larger than available memory, loading the whole thing with 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.
Question 4 of 5

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?

Text files can be encoded in different ways (UTF-8, Latin-1, etc.). Reading with the wrong assumed encoding raises UnicodeDecodeError โ€” another case for Session 11's try/except, or for specifying the correct encoding explicitly if it is known.
Question 5 of 5

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?

This is Session 28's payoff, delivered a third time (after the mock data and the simple JSON file in Session 29): only the data SOURCE changes; get_all() and find_by_region() need zero modification, because they were never coupled to how raw_data was originally obtained.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. 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?
  2. 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?
  3. 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?
  4. 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.