15  Python try and except statements

TipLearning Objectives
  • Understand how try and except statements manage runtime errors in Python.
  • Learn how to use the except Exception as e idiom to catch and inspect non-named or unexpected errors.
  • Learn the full syntax and execution flow of try, except, else, and finally blocks.
  • Incorporate exception handling into your repertoire of Python programming constructs to build robust, crash-resistant applications.

15.1 try and except in Python

In Python, try and except blocks are used for handling exceptions, which are errors that can occur during program execution. This mechanism allows developers to anticipate potential errors, provide alternate code to handle them, and prevent the program from crashing.

A Simple Example

Suppose we want to convert a text string into an integer. If the string contains non-numeric characters, Python raises a ValueError. We can catch this error using try and except:

user_input = "twenty"

try:
    number = int(user_input)
    print(f"Successfully converted number: {number}")
except ValueError:
    print(f"Error: '{user_input}' is not a valid integer!")
Error: 'twenty' is not a valid integer!

If user_input were "20", the code inside the try block would succeed and print the result. Because user_input is "twenty", Python skips directly to the except ValueError block and displays a helpful message rather than stopping execution with an unhandled exception.

Catching Unnamed Exceptions with except Exception as e

While catching specific errors (like ValueError or ZeroDivisionError) is recommended, you cannot always predict every error that might occur. In Python, Exception is the base class for almost all non-fatal errors.

The idiom except Exception as e allows you to catch any standard exception and store the exception object in a variable named e. This allows you to inspect, print, or log the exact error message that Python generated:

items = [10, 20, 30]

try:
    # Attempting to perform an operation that fails
    result = items["first"]  # Passing a string key to a list causes a TypeError
except Exception as e:
    print(f"An unexpected error occurred!")
    print(f"Exception type: {type(e).__name__}")
    print(f"Error details: {e}")
An unexpected error occurred!
Exception type: TypeError
Error details: list indices must be integers or slices, not str

Key Rules for except Exception as e:

  • Use as a Fallback: Place general except Exception as e blocks after specific exception blocks so that known errors are handled specifically first.
  • Debugging & Logging: Printing e gives valuable insight into what went wrong without allowing the program to crash.

15.2 The Full Structure of Exception Handling

try Block:

The code that might raise an exception is placed inside the try block. If an exception occurs during execution of this block, the rest of the block is skipped, and control is passed to the except block.

except Block:

This block contains code that handles the exception. You can specify which exception to catch, or leave it blank to catch any exception. If the exception type matches the one specified in the except block, the code inside it is executed.

Multiple except Blocks:

You can have multiple except blocks to handle different types of exceptions.

else Block:

You can add an else block after the except block. This block runs if the try block executes without raising an exception.

finally Block:

The finally block runs regardless of whether an exception was raised or not. It is often used for cleanup actions, like closing files or releasing resources.

Here is a comprehensive example demonstrating all four components:

try:
    numerator = 10
    denominator = 0
    result = numerator / denominator
except ZeroDivisionError:
    # Handling a specific division-by-zero exception
    print("Error: You cannot divide by zero.")
except Exception as e:
    # Catching any other unexpected exception using the idiom
    print(f"An unexpected error occurred ({type(e).__name__}): {e}")
else:
    # Executes only if no exception occurred
    print("The result is:", result)
finally:
    # This block runs no matter what
    print("Execution completed.")
Error: You cannot divide by zero.
Execution completed.

Explanation of the Example:

try:

  • Attempts to divide numerator by denominator. Since denominator is zero, this raises a ZeroDivisionError.

except:

  • The first except catches the ZeroDivisionError and prints an error message.
  • The second except would catch any other unexpected exceptions, but it won’t run in this case because the first except handles the error.

else:

  • If there were no exceptions, the else block would print the result.

finally:

  • This block runs at the end of the try/except structure, regardless of whether an exception occurred or not. It’s useful for cleanup actions.

In this case I might not use the else or finally

Benefits of Using try and except:

  • Prevents Crashes: By handling exceptions, you can prevent your program from crashing due to unforeseen errors.
  • Cleaner Code: It allows for clearer separation of normal code and error handling.
  • More Robust Programs: By anticipating and handling potential errors, your programs can handle unexpected situations better.

15.3 Exercises and Solutions

ExerciseExercise 1 - Exercise 1: Safe List Indexing (Beginner)

Level:

Write a function get_element(data_list, index) that returns the item at the given index from data_list. Use a try...except block to catch out-of-bounds index errors (IndexError) as well as non-integer index inputs using except Exception as e.

Answer

def get_element(data_list, index):
    try:
        value = data_list[index]
        return f"Found value: {value}"
    except IndexError:
        return f"Error: Index {index} is out of range for list of length {len(data_list)}."
    except Exception as e:
        return f"An error occurred ({type(e).__name__}): {e}"

# Testing the function
sample_list = ["apple", "banana", "cherry"]

print(get_element(sample_list, 1))       # Valid index
print(get_element(sample_list, 10))      # Triggers IndexError
print(get_element(sample_list, "one"))   # Triggers TypeError caught by Exception as e
Found value: banana
Error: Index 10 is out of range for list of length 3.
An error occurred (TypeError): list indices must be integers or slices, not str

Explanation:

  1. Valid indices (e.g. 1) execute cleanly.

  2. An index beyond the length of the list (e.g. 10) triggers IndexError.

  3. Passing a string as an index (e.g. "one") triggers a TypeError, which falls through to except Exception as e.

ExerciseExercise 2 - Exercise 2: Count Nucleotide Occurrences

Level:

Given a DNA sequence, count the occurrences of valid nucleotides (A, T, C, G). What exceptions might occur? Use try and except to deal with them.

Use a dictionary and try...except to catch errors rather than a long chain of if-elif statements.

Answer

You might have initially been inclined to write something like the following:

dna_sequence = "ATCGATCGATCGATCG"

# Initialize counts
count_A = 0
count_T = 0
count_C = 0
count_G = 0

# Count nucleotides
for nucleotide in dna_sequence:
    if nucleotide == 'A':
        count_A += 1
    elif nucleotide == 'T':
        count_T += 1
    elif nucleotide == 'C':
        count_C += 1
    elif nucleotide == 'G':
        count_G += 1

print(f"A: {count_A}, T: {count_T}, C: {count_C}, G: {count_G}")

However these DNA sequences might include invalid characters like X or N.

When storing counts in a dictionary, attempting to access an invalid key directly raises a KeyError. We can catch this with try...except:

dna_sequence = "ATCGATCGXATCGATCGN"  # Contains invalid bases 'X' and 'N'

counts = {'A': 0, 'T': 0, 'C': 0, 'G': 0}
invalid_counts = {}

for position, nucleotide in enumerate(dna_sequence):
    try:
        counts[nucleotide] += 1
    except KeyError:
        # Catch unexpected nucleotides not present in our dictionary
        invalid_counts[nucleotide] = invalid_counts.get(nucleotide, 0) + 1

print("Valid Base Counts:", counts)
print("Invalid Base Counts:", invalid_counts)
Valid Base Counts: {'A': 4, 'T': 4, 'C': 4, 'G': 4}
Invalid Base Counts: {'X': 1, 'N': 1}

Key Advantages:

  • Avoids long if-elif conditional structures.
  • Safely records invalid characters without halting program execution.
ExerciseExercise 3 - Exercise 3: if Statements vs try / except

Level:

Discussion: Can if statements completely replace try / except blocks?

Answer

No! While if statements and try...except blocks both guide program flow, they serve fundamentally distinct purposes:

“Look Before You Leap” (LBYL) vs “Easier to Ask for Forgiveness than Permission” (EAFP):

  • if statements check conditions before executing an action (LBYL).

  • try...except attempts an action and handles failures if they occur (EAFP)

Unforeseen Failures:

if statements can only be used to deal with errors you have forseen and that can be checkes easily. They cannot be used for all eventualities and unexpected occurrences. For example:

  • An if statement can check if a file exists (if os.path.exists("file.txt"):), but the file could still be deleted, locked, or corrupted between the check and the actual read operation.

  • A try...except block safely handles FileNotFoundError or PermissionError during the operation itself.

Data Type Parsing:

  • Validating complex strings before numeric conversion using if statements often requires cumbersome regular expressions. Using try: float(value) is much cleaner and more reliable.
ExerciseExercise 4 - Exercise 4: Improving the Expense Calculator

Level:

Where might you add try and except statements in the improved collect_expenses() function from input_handler.py that we were looking at?

Implement these statements in a working script, and discuss when you might prefer not to use try...except.

Answer

There are two separate places worth a try...except in collect_expenses(), for two different reasons:

  1. Converting the amount to a number (float(amount_str)) — the current code avoids a crash here using amount_str.replace('.', '', 1).isdigit() to check the string first, but this is a bit clunky and doesn’t catch every valid numeric format (e.g. negative numbers, or a leading +). A try...except ValueError around the conversion itself is cleaner and more robust than trying to pre-validate the string.

  2. Calling the passed-in functions (get_total_func, check_logic_func) — these are parameters, not fixed implementations, so collect_expenses() doesn’t control what runs inside them. A caller could pass in a version that does thresholds[category] instead of thresholds.get(category), or has some other bug. That’s a genuine unexpected runtime error crossing into code this function doesn’t own — exactly the case try...except is for.

Using both means the loop can tell the difference between “the user typed something invalid” (handled with the normal invalid-input counter) and “something went wrong inside code we don’t control” (caught, reported, and the loop continues).

def get_category_total(expenses, category):
    """Simple math logic to filter and sum expenses."""
    return sum(amount for cat, amount in expenses if cat == category)

def check_threshold(category, current_total, thresholds):
    """Handles the conditional logic for budget warnings."""
    limit = thresholds.get(category, 0)
    if current_total > limit:
        print(f"Warning: You have exceeded the {category} threshold!")
        print(f"Total spent in {category}: {current_total}")
    else:
        remaining = limit - current_total
        print(f"Added to {category}.")
        print(f"Total spent in {category}: {current_total}")
        print(f"Amount left for {category}: {remaining}\n")

def collect_expenses(thresholds, expenses, get_total_func, check_logic_func, max_entries=10, max_invalid_in_a_row=3):
    """Uses a while loop to interact with the user, with limits and error handling added."""
    print("Enter your expenses. Type 'done' to finish.\n")

    entry_count = 0
    invalid_count = 0

    while entry_count < max_entries:
        category = input("Category (food, transport, entertainment, other): ").lower()
        if category == "done":
            break

        amount_str = input("Amount: ")

        if category not in thresholds:
            invalid_count += 1
            print("Invalid category. Try again.\n")
            if invalid_count >= max_invalid_in_a_row:
                print("Too many invalid entries in a row. Stopping.")
                break
            continue

        # try/except used here instead of a manual string check,
        # since float() is the operation that can actually fail
        try:
            amount = float(amount_str)
        except ValueError:
            invalid_count += 1
            print("Invalid amount. Please enter a number.\n")
            if invalid_count >= max_invalid_in_a_row:
                print("Too many invalid entries in a row. Stopping.")
                break
            continue

        expenses.append([category, amount])
        entry_count += 1
        invalid_count = 0  # reset on a valid entry

        # Use the passed-in logic functions — wrapped separately, since a
        # failure here means a bug in code we don't own, not bad user input
        try:
            current_total = get_total_func(expenses, category)
            check_logic_func(category, current_total, thresholds)
        except Exception as e:
            print(f"Unexpected error while processing '{category}': {e}\n")

    if entry_count >= max_entries:
        print("Maximum number of expense entries reached.")

When NOT to use try...except:

  • For Syntax Errors or Logic Bugs: Do not hide programming mistakes with try...except; fix the code logic instead.

  • Overly Broad Catching Without Logging: Avoid silent except: or except Exception: blocks that pass without action (pass), as this hides critical errors (like NameError or variable misspelling) and makes debugging difficult.

  • Simple Predictable Control Flow: If checking. When a certain value is expected some of the time and requires alternate processing, prefer using if statements rather than try and except


15.4 Summary

TipKey Points
  • try and except blocks prevent Python scripts from crashing when runtime errors occur.
  • Use except Exception as e to gracefully handle unexpected or unnamed exceptions and inspect the error message stored in e.
  • else blocks execute when no exceptions occur, while finally blocks always execute, making them ideal for cleanup operations.
  • Prefer specific exception catching (e.g. except ValueError:) before general fallbacks (except Exception as e:).
  • Combine if statements for standard logical branching with try...except for handling unpredictable data inputs and execution errors.