0%

When the Outside World Goes Wrong · practice

Catch Where the Failure Has Meaning

The inclusive filter is repaired. The command still has one over-wide boundary inherited from Chapter 8:

try:
    document = convert_catalog(input_path, output_path, minimum_quantity)
except (FileNotFoundError, UnicodeDecodeError, csv.Error, ValueError) as exc:
    ...

That names only expected , but the try block spans reading, selection, validation, and writing. Location matters as much as class. A from parsing a damaged CSV row is expected external input. A ValueError from the writer’s validation means project code handed its own writer an invalid internal document. Normalizing both would erase that distinction.

Put a boundary around one understood operation

Create run_catalog(input_path, output_path, minimum_quantity). Its first boundary surrounds only the read:

try:
    items = read_catalog_csv(input_path)
except (OSError, UnicodeDecodeError, csv.Error, ValueError) as exc:
    print(f"Could not read {input_path}: {exc}", file=sys.stderr)
    return 1

OSError is the operating-system family that includes missing paths, permission failures, and other failed file operations. Here it has one meaning: the requested input could not be opened or read. UnicodeDecodeError, csv.Error, and ValueError mean the bytes, CSV syntax, or declared schema could not become a catalog.

The message includes the exact input path and the bounded cause. stdout stays empty. Most importantly, the writer has not run, so a pre-existing output file remains byte-for-byte unchanged.

Run selection after that handler, outside every try:

selected = select_items(items, minimum_quantity)

If selection raises KeyError, TypeError, ValueError, RuntimeError, or even OSError, the original exception escapes. Selection does not touch the outside world, so calling its defect a read or write failure would be false.

Give the output operation its own boundary

Surround only the writer:

try:
    document = write_catalog_json(selected, output_path)
except OSError as exc:
    print(f"Could not write {output_path}: {exc}", file=sys.stderr)
    return 1

Only an operating-system write failure is expected here. A ValueError or TypeError from validation is an internal invariant failure and must escape unchanged.

Print the success line only after write_catalog_json returns. “Wrote N items” is a claim about a completed operation, so printing it earlier would create a success-shaped lie.

The honest artifact limit

This project writes directly to the requested path. If the fails before opening it, an old file may remain unchanged. If failure happens after truncation or after some bytes were written, the path may be absent or partial. This chapter does not promise rollback it has not implemented.

There is also no automatic retry. Retrying a missing path or permission error without changing anything merely repeats the same case. The diagnostic gives the caller evidence; it does not invent a recovery policy.

Update main(argv=None) so argparse still owns help and grammar errors, then forward its three parsed values to run_catalog. The complete contract is:

Situationstdoutstderrstatus
successWrote N items to PATH.empty0
read/input failureemptyCould not read INPUT: cause1
output OSErroremptyCould not write OUTPUT: cause1
parser erroremptyargparse usage and error2
unexpected defectnone promisedoriginal nonzero

Implement the two operation-sized boundaries.

The test of whether you got it right is not whether the program stops crashing. It is whether the same exception class gets different treatment depending on where it came from. A ValueError from the reader should produce a short message; a ValueError from the writer’s own validation should still produce a traceback. If one broad try covers both, you cannot tell those two apart, and neither can the person running your program.

A ValueError reaches your code. Where it came from decides what to do. Why?

Why does select_items run outside every try block?

Task

Refactor the orchestration into run_catalog(input_path, output_path, minimum_quantity).

  • Catch OSError, UnicodeDecodeError, csv.Error, and only around read_catalog_csv, then print Could not read INPUT_PATH: CAUSE to stderr and return 1.

  • Run select_items outside every try block.

  • Catch only OSError around write_catalog_json, then print Could not write OUTPUT_PATH: CAUSE to stderr and return 1.

  • After a successful write, print Wrote N items to OUTPUT_PATH. to stdout and return 0.

  • Make main(argv=None) parse and return run_catalog(...).

Keep argparse help/status 0 and grammar/status 2. Let selection defects and writer validation errors escape unchanged. Do not add retries or rollback claims.