DEV Community

Cover image for The Alert System: Try, Except, and Finally
Aaron Rose
Aaron Rose

Posted on

The Alert System: Try, Except, and Finally

Timothy watched in horror as the entire catalog system crashed. He'd been importing book records from a partner library when a single malformed entry—a book with no publication year—brought everything to a halt. The error message scrolled past, the program died, and three hours of import work vanished.

Margaret found him staring at the error traceback. "You need the Alert System," she said, leading him to a corridor lined with ornate bell pulls and emergency protocols. "When something goes wrong, you don't want the entire library to shut down. You need to handle problems gracefully."

The Crash Problem

Timothy's import code was brittle:

# Note: Examples use placeholder functions like add_to_catalog(), update_index()
# In practice, replace these with your actual implementation

def import_book_record(record):
    title = record['title']
    author = record['author']
    year = int(record['year'])  # Crashes if 'year' is missing or invalid!

    add_to_catalog(title, author, year)
    update_index(title)
    log_import(title)
    return "Success"

# Process thousands of records
for record in partner_library_data:
    import_book_record(record)  # One bad record kills everything
Enter fullscreen mode Exit fullscreen mode

A single missing field or invalid value crashed the entire import. Thousands of valid records were lost because one was malformed.

"Exceptions," Margaret explained, "are Python's way of signaling problems. But if you don't handle them, they crash your program. The Alert System lets you catch problems, decide how to respond, and keep operating."

The Try-Except Structure

Margaret showed Timothy the basic pattern:

def import_book_record(record):
    try:
        title = record['title']
        author = record['author']
        year = int(record['year'])

        add_to_catalog(title, author, year)
        update_index(title)
        log_import(title)
        return "Success"
    except KeyError as e:
        print(f"Missing required field: {e}")
        return "Failed - missing field"
    except ValueError as e:
        print(f"Invalid data format: {e}")
        return "Failed - invalid data"

# Now one bad record doesn't kill the import
for record in partner_library_data:
    result = import_book_record(record)
    if result.startswith("Failed"):
        log_failed_import(record)
Enter fullscreen mode Exit fullscreen mode

The try block contained code that might fail. The except blocks caught specific exceptions and handled them appropriately. The import continued even when individual records failed.

"Think of it," Margaret said, "like emergency bells throughout the library. Each type of problem rings a different bell, and staff respond according to established protocols."

Catching Multiple Exception Types

Timothy learned to handle different problems differently:

def process_catalog_entry(book_data):
    try:
        with open(f"catalog/{book_data['id']}.txt", 'r') as f:
            existing = f.read()

        updated = merge_book_data(existing, book_data)

        with open(f"catalog/{book_data['id']}.txt", 'w') as f:
            f.write(updated)

    except FileNotFoundError:
        # File doesn't exist - create new entry
        create_new_catalog_entry(book_data)

    except PermissionError:
        # Can't write - log for admin attention
        log_permission_error(book_data['id'])

    except KeyError as e:
        # Missing required field - skip this entry
        log_malformed_data(book_data, missing_field=e)
Enter fullscreen mode Exit fullscreen mode

Each except clause handled a specific exception type with appropriate logic. File doesn't exist? Create it. Permission denied? Log for admin. Malformed data? Skip it.

The Exception Hierarchy

Margaret revealed an important detail: exceptions form a hierarchy.

try:
    result = risky_operation()
except OSError:
    # Catches FileNotFoundError, PermissionError, and other OS errors
    handle_file_problem()
except ValueError:
    # Catches int() conversion failures, invalid formats
    handle_data_problem()
Enter fullscreen mode Exit fullscreen mode

OSError was a parent class of FileNotFoundError and PermissionError. Catching the parent caught all children. This let Timothy write more general error handling when he didn't need specific responses.

"Be careful though," Margaret cautioned. "Catching too broadly hides problems."

try:
    process_catalog()
except Exception:  # Catches most things - still too broad!
    print("Something went wrong")
    # What went wrong? Where? Why? We don't know!
Enter fullscreen mode Exit fullscreen mode

The overly-broad except Exception caught almost every error, making debugging difficult. But Margaret showed him something even worse:

try:
    process_catalog()
except:  # NEVER do this - bare except!
    print("Error occurred")
Enter fullscreen mode Exit fullscreen mode

"A bare except with no exception type," Margaret warned, "catches everything, including SystemExit and KeyboardInterrupt. Your program can't be shut down properly—even Ctrl+C won't work!"

# Dangerous - catches system signals
try:
    long_running_process()
except:  # User presses Ctrl+C... program ignores it!
    pass

# Better - doesn't catch system exits
try:
    long_running_process()
except Exception:  # Ctrl+C still works
    log_error()
Enter fullscreen mode Exit fullscreen mode

"Never use bare except," Margaret emphasized. "At minimum, use except Exception—and even that should be rare. Be specific about what you catch."

The Python Philosophy: EAFP

Margaret introduced Timothy to a Python idiom: "Easier to Ask Forgiveness than Permission."

# "Look Before You Leap" (LBYL) - check first
import os

if os.path.exists(filename):
    with open(filename) as f:
        data = f.read()
else:
    data = ""

# "Easier to Ask Forgiveness" (EAFP) - just try it
try:
    with open(filename) as f:
        data = f.read()
except FileNotFoundError:
    data = ""
Enter fullscreen mode Exit fullscreen mode

"The Pythonic way," Margaret explained, "is to try the operation and handle the exception if it fails. It's often cleaner and avoids race conditions—the file could be deleted between your check and your open."

The Finally Block

Timothy discovered a problem: cleanup code sometimes didn't run.

def update_catalog_with_lock():
    acquire_catalog_lock()
    try:
        modify_catalog()
        return "Success"
    except ValueError:
        return "Failed"
    # Lock never released if exception occurs!
Enter fullscreen mode Exit fullscreen mode

Whether the operation succeeded or failed, the lock needed release. Margaret showed him finally:

def update_catalog_with_lock():
    acquire_catalog_lock()
    try:
        modify_catalog()
        return "Success"
    except ValueError:
        return "Failed"
    finally:
        release_catalog_lock()  # ALWAYS runs, even with return statements
Enter fullscreen mode Exit fullscreen mode

The finally block executed no matter what—success, failure, return, or even if another exception occurred. It guaranteed cleanup.

"Finally is like the library's closing bell," Margaret explained. "No matter what happened during the day, we always lock the doors at closing time."

The Else Clause

Margaret revealed a lesser-known feature:

def import_record(record):
    try:
        book = parse_record(record)
        validate_book(book)
    except ValueError as e:
        log_error(f"Invalid record: {e}")
    else:
        # Only runs if NO exception occurred
        add_to_catalog(book)
        send_success_notification()
Enter fullscreen mode Exit fullscreen mode

The else block ran only when the try block succeeded without exceptions. It separated success logic from the risky operations, making code more readable.

Re-raising Exceptions

Timothy discovered an important pattern: sometimes you need to handle an exception but still let it propagate.

import logging

def import_book_with_logging(record):
    try:
        parse_and_import(record)
    except ValueError as e:
        # Log it for debugging
        logging.error(f"Failed to import record {record.get('id')}: {e}")
        # But let the caller decide how to handle it
        raise  # Re-raises the same exception
Enter fullscreen mode Exit fullscreen mode

"The bare raise statement," Margaret explained, "re-raises the current exception. You've logged it, but you're passing it up to let the caller decide the appropriate response."

Timothy could also transform exceptions:

class CatalogError(Exception):
    """Custom exception for catalog operations"""
    pass

def import_book(record):
    try:
        year = int(record['year'])
    except ValueError as e:
        # Transform into domain-specific exception
        raise CatalogError(f"Invalid year format in record") from e
        # The 'from e' preserves the original exception context
Enter fullscreen mode Exit fullscreen mode

The from e syntax preserved the original exception, creating a chain that showed both what went wrong initially and the higher-level problem it caused.

The Retry Pattern

Timothy built a pattern for handling temporary failures:

import time

# Note: NetworkError is a placeholder - in practice, use actual network exceptions
# like requests.exceptions.RequestException

def fetch_remote_catalog(max_attempts=3):
    for attempt in range(max_attempts):
        try:
            data = download_from_partner_library()
            return data
        except NetworkError as e:
            if attempt == max_attempts - 1:
                # Final attempt failed - give up
                raise
            print(f"Attempt {attempt + 1} failed, retrying...")
            time.sleep(2 ** attempt)  # Exponential backoff
Enter fullscreen mode Exit fullscreen mode

The pattern tried multiple times with increasing delays. Only if all attempts failed did it raise the exception.

The Fallback Value Pattern

Margaret showed Timothy how to provide defaults when operations failed:

def get_book_count(catalog_file):
    try:
        with open(catalog_file, 'r') as f:
            return len(f.readlines())
    except FileNotFoundError:
        return 0  # Catalog doesn't exist - count is zero

def get_setting(key, default=None):
    try:
        return config[key]
    except KeyError:
        return default  # Setting not found - use default
Enter fullscreen mode Exit fullscreen mode

Instead of crashing, the functions returned sensible defaults when problems occurred.

Logging Errors Properly

Timothy learned to preserve error information:

import logging

def process_catalog_batch(records):
    for record in records:
        try:
            import_book(record)
        except Exception as e:
            # Log the full error with context
            logging.error(
                f"Failed to import book {record.get('id', 'unknown')}: {e}",
                exc_info=True  # Includes full traceback in logs
            )
            continue  # Process remaining records
Enter fullscreen mode Exit fullscreen mode

The exc_info=True parameter captured the full exception traceback in logs, making debugging easier without crashing the program.

Exception Best Practices

Margaret compiled Timothy's lessons into principles:

Be specific about what you catch:

# Good - catches specific expected problems
try:
    year = int(book['year'])
except (KeyError, ValueError):
    year = None

# Bad - catches everything, even bugs
try:
    year = int(book['year'])
except Exception:  # Too broad
    year = None

# Terrible - catches system exits too!
try:
    year = int(book['year'])
except:  # NEVER use bare except
    year = None
Enter fullscreen mode Exit fullscreen mode

Don't hide errors silently:

# Bad - errors vanish without a trace
try:
    critical_operation()
except Exception:
    pass

# Good - at minimum, log what happened
try:
    critical_operation()
except Exception as e:
    logging.error(f"Critical operation failed: {e}")
    raise  # Re-raise to let caller handle
Enter fullscreen mode Exit fullscreen mode

Use context managers for cleanup:

# Exception-safe with context manager
with open('catalog.txt') as f:
    process(f)

# Manual cleanup with finally
f = open('catalog.txt')
try:
    process(f)
finally:
    f.close()
Enter fullscreen mode Exit fullscreen mode

Timothy's Exception Handling Wisdom

Through mastering the Alert System, Timothy learned essential principles:

Try blocks should be small: Only wrap code that might actually raise exceptions.

Catch specific exceptions: Never use bare except. Rarely use except Exception. Be specific.

Finally guarantees cleanup: Use it for releasing resources, not for error handling.

Else separates success from risky code: Makes the success path clear.

Log errors with context: Include enough information to debug later.

Re-raise when appropriate: Log, clean up, then let the exception propagate with raise.

Provide fallbacks when sensible: Return defaults rather than crashing for expected failures.

Use exception chaining: raise CustomError() from original_error preserves context.

Follow EAFP: Try the operation and handle exceptions rather than checking conditions first.

Let unexpected errors propagate: If you don't know how to handle it, let it crash (with good logging).

Timothy's exploration of exception handling transformed his error-prone import scripts into robust, production-ready systems. The Alert System didn't prevent problems—it ensured that when problems occurred, the library stayed operational, errors were logged for investigation, and valid work continued. The emergency bells rang when needed, staff responded appropriately, and the library kept serving patrons.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)