FREE LESSON · Programming & program design · 7 OF 8

Exceptions, files, and resource lifetimes

Exceptions, files, and resource lifetimes: Handle expected failures while preserving useful errors and releasing resources.

Prerequisites: Functions, lists, strings, and basic exceptions.

Learning objectives

  • Distinguish failure categories and handle them at appropriate boundaries.
  • Use context management to release resources.
  • Separate parsing, validation, and side effects.

Failure is part of the interface

Programs encounter invalid input, missing files, permissions failures, and interrupted operations. An exception transfers control from a failing operation to a matching handler. If no local handler applies, it propagates to the caller. This is different from returning an ordinary value: the rest of the current straight-line computation does not automatically run.

Handle failures where you can make a meaningful decision. A parser can attach a line number to malformed data. A command-line boundary can present a readable error and choose an exit status. A low-level function that catches every exception and returns zero destroys the distinction between valid zero and failed computation, while also hiding programming bugs.

The following standard Python example separates conversion from file opening. It uses an in-memory text stream so it runs without creating a file.

from io import StringIO

def read_scores(stream):
    scores = []
    for line_number, line in enumerate(stream, 1):
        try:
            score = int(line.strip())
        except ValueError as error:
            raise ValueError(f"invalid score on line {line_number}") from error
        if not 0 <= score <= 100:
            raise ValueError(f"score outside range on line {line_number}")
        scores.append(score)
    return scores

with StringIO("12\n90\n") as source:
    assert read_scores(source) == [12, 90]
assert source.closed

Acquisition and release should be paired

Real files consume operating-system resources. A context manager, used with with, arranges cleanup when the block exits normally or because an exception propagates. A real caller could open a path with an explicit text encoding and pass that stream to read_scores. The function receiving the stream should not unexpectedly close a resource owned by its caller unless that ownership transfer is documented.

Exception chaining preserves the original cause while adding domain context. Catch the narrow failure you intend to translate. A broad handler around a large block may mistake an unrelated defect for bad user input. Also distinguish syntactic parsing from semantic validation: an integer conversion accepts 900, but the score contract rejects it.

Partial effects require a policy

Closing a file does not undo writes already made. If a batch operation must be all-or-nothing, first validate into temporary state, then use an appropriate commit strategy. Replacing a file atomically and guaranteeing persistence through a crash are separate system-level concerns. State what your function actually guarantees. Good tests include malformed lines, range failures, empty input, resource cleanup, and whether any externally visible state changes before failure.

Practice problem

Design an importer for lines containing a username and a nonnegative score separated by a comma. The importer must reject malformed lines with a line number and must not update the destination dictionary unless the entire input is valid. State a policy for duplicate usernames and blank lines. Describe how to separate parsing from committing, which exceptions to catch, and who owns the input stream. Give an input with a valid first line and invalid second line that would expose premature updates.

Worked solution

Choose explicit policies, such as rejecting blank lines and duplicate usernames. Parse into a new temporary dictionary, checking exactly two fields, a nonempty username, integer conversion, and a nonnegative score. Catch conversion ValueError narrowly and raise a contextual validation error with the line number while preserving its cause. Only after every line succeeds should the caller apply the complete result to the destination according to a documented replacement or merge policy. Input ada,8 followed by bo,wrong must leave the destination unchanged. The caller that opened the stream owns closing it, normally with a context manager. Do not catch unrelated TypeError or resource errors as though they were malformed records. This design provides in-memory all-or-nothing validation; durable database or filesystem atomicity requires a separate commit mechanism.

Check your understanding

What does a with block around a file primarily guarantee?

  • Every write is rolled back if an exception occurs.
  • The file is closed when control leaves the block.
  • The data is durable after any power loss.
Explanation

Context management pairs acquisition with cleanup. Rollback and durable persistence are separate guarantees.

Why catch ValueError around integer conversion rather than every exception around the whole importer?

  • It preserves unrelated defects and resource failures instead of misclassifying them as bad input.
  • It ensures every file is valid.
  • It automatically makes the import atomic.
Explanation

A narrow handler translates the failure it understands while allowing other failures to retain their meaning.

Primary reading

Open the study and save your work →