Chapter 8 Exceptions, Files, and Paths

Reliable Python programs must handle invalid input and external resources deliberately. Interview questions often use file operations to test whether you understand exceptions, cleanup, and reproducibility—not just syntax.

8.1 Exceptions communicate failure

An exception interrupts the normal control flow and carries information about what failed. Catch only errors you can handle meaningfully. Avoid a bare except, which can hide programming errors and make debugging harder.

def parse_nonnegative_integer(value):
    """Return a nonnegative integer or raise a clear ValueError."""
    try:
        number = int(value)
    except (TypeError, ValueError) as exc:
        raise ValueError(f"Expected an integer-like value, received {value!r}") from exc

    if number < 0:
        raise ValueError("The value must be nonnegative")

    return number


assert parse_nonnegative_integer("12") == 12

This function catches only the conversion errors it expects. It then raises a separate domain error for negative values. The raise ... from exc syntax preserves the original cause for debugging.

8.2 Handling an expected failure

Catch an exception near the place where you can decide what to do about it. Returning a fallback value may be appropriate for optional user input, but it is usually inappropriate for corrupted training data or failed model output.

try:
    parsed_value = parse_nonnegative_integer("not a number")
except ValueError as exc:
    parsed_value = None
    error_message = str(exc)

assert parsed_value is None
assert "Expected an integer-like value" in error_message

8.3 else and finally

  • else runs only when the try block succeeds.
  • finally runs whether the operation succeeds or fails, so it is useful for cleanup that cannot be expressed with a context manager.

Prefer context managers for files, database connections, locks, and similar resources because they make cleanup automatic.

8.4 Paths with pathlib

pathlib.Path represents filesystem paths as objects and avoids fragile string concatenation. Build paths relative to a known project or data directory rather than embedding machine-specific paths such as /Users/name/Desktop/....

from pathlib import Path

data_path = Path("__REPO") / "data" / "college.csv"

assert data_path.exists()
assert data_path.suffix == ".csv"

For an application, define the project root once and pass paths into functions. Do not make reusable functions depend silently on the current working directory.

8.5 Safe text-file handling

The with statement closes the file even if reading or processing raises an exception. Specify the text encoding explicitly.

from tempfile import TemporaryDirectory

with TemporaryDirectory() as temporary_directory:
    output_path = Path(temporary_directory) / "example.txt"

    with output_path.open("w", encoding="utf-8") as output_file:
        output_file.write("alpha\n")
        output_file.write("beta\n")

    with output_path.open("r", encoding="utf-8") as input_file:
        lines = [line.strip() for line in input_file]
## 6
## 5

assert lines == ["alpha", "beta"]

The temporary directory keeps this example reproducible and prevents the book build from leaving files in the repository.

8.6 Separate input/output from transformation

A useful design pattern is to keep file access thin and put transformation logic in a function that can be tested without the filesystem.

def normalize_nonempty_lines(lines):
    return [line.strip().lower() for line in lines if line.strip()]


raw_lines = [" Alpha ", "", "BETA\n"]
assert normalize_nonempty_lines(raw_lines) == ["alpha", "beta"]

This separation makes unit tests fast and allows the same transformation to be used with files, network responses, or in-memory data.

8.7 Structured data and serialization

Use a documented, interoperable format such as JSON for simple structured data. Validate the loaded structure before trusting it. Python’s pickle format can reconstruct Python objects, but unpickling untrusted data can execute malicious code; never treat an unknown pickle as ordinary data.

import json

record = {"experiment": "checkout_test", "sample_size": 2500}

with TemporaryDirectory() as temporary_directory:
    json_path = Path(temporary_directory) / "record.json"

    with json_path.open("w", encoding="utf-8") as output_file:
        json.dump(record, output_file, indent=2)

    with json_path.open("r", encoding="utf-8") as input_file:
        loaded_record = json.load(input_file)

assert loaded_record == record

Path.iterdir() and Path.glob() support directory inspection without manual string manipulation. Sort results when deterministic order matters.

with TemporaryDirectory() as temporary_directory:
    directory = Path(temporary_directory)
    (directory / "b.csv").touch()
    (directory / "a.csv").touch()
    (directory / "notes.txt").touch()

    csv_names = sorted(path.name for path in directory.glob("*.csv"))

assert csv_names == ["a.csv", "b.csv"]

8.8 Interview checklist

  • Which specific exceptions can this operation raise?
  • Can the program recover, or should the exception propagate?
  • Is cleanup guaranteed?
  • Is the path portable across machines and operating systems?
  • Is text encoding explicit?
  • Is the serialization format safe for the data’s source and consumers?
  • Can the transformation be tested without performing I/O?

8.9 Exercises

  1. Extend parse_nonnegative_integer() to reject non-integral floats such as 3.5 rather than truncating them.
  2. Write a function that reads a UTF-8 text file and counts nonblank lines.
  3. Explain why catching Exception and returning None can make a data pipeline silently publish incorrect results.