Programming with Python | Chapter 11: Error Handling and Exceptions

Chapter Objectives

  • Understand the difference between syntax errors and exceptions (runtime errors).
  • Recognize common built-in Python exceptions (e.g., TypeError, ValueError, IndexError, KeyError, FileNotFoundError, ZeroDivisionError).
  • Learn how to handle exceptions gracefully using the try...except block.
  • Catch specific types of exceptions.
  • Access the exception object for more details.
  • Use the optional else block with try...except to run code only when no exception occurs.
  • Use the optional finally block to execute cleanup code regardless of whether an exception occurred.
  • Learn how to raise exceptions intentionally using the raise statement.
  • Understand the basic concept of custom exceptions (though detailed implementation is an advanced topic).

Introduction

Even well-written programs can encounter unexpected situations during execution. For example, trying to divide by zero, accessing a non-existent file, converting invalid input to a number, or using an incorrect index for a list can all cause runtime errors. Python uses a mechanism called exceptions to signal and handle these errors. If an exception occurs and is not handled, the program crashes. This chapter introduces exception handling, showing you how to anticipate potential errors and write code (using try, except, else, and finally) that can gracefully manage these situations, preventing crashes and allowing your program to continue running or terminate cleanly.

Theory & Explanation

Syntax Errors vs. Exceptions

It’s important to distinguish between two main types of errors:

  1. Syntax Errors (Parsing Errors): These occur before the program starts running. They happen when the code violates Python’s grammatical rules (e.g., missing colons, incorrect indentation, misspelled keywords). The Python interpreter detects these during the parsing phase and won’t even begin execution. You must fix syntax errors before the program can run at all.# Syntax Error example (missing colon) # if x > 5 # print("Greater")
  2. Exceptions (Runtime Errors): These occur during the execution of the program, even if the syntax is correct. They arise when something unexpected happens, like trying an operation that isn’t valid for a given value or encountering an external issue (like a missing file).# Exception example (ZeroDivisionError) # numerator = 10 # denominator = 0 # result = numerator / denominator # Raises ZeroDivisionError at runtime
    This chapter focuses on handling exceptions.

Common Built-in Exceptions

Python has many built-in exceptions representing different error types. Some common ones include:

Exception Name Description Example Trigger
TypeError An operation or function is applied to an object of an inappropriate type. 'hello' + 5
ValueError An operation or function receives an argument of the correct type but an inappropriate value. int('abc')
IndexError A sequence subscript (index) is out of range. my_list = [1, 2]
print(my_list[5])
KeyError A dictionary key is not found. my_dict = {'a': 1}
print(my_dict['b'])
FileNotFoundError Attempting to open a file that does not exist (in read mode). open('non_existent.txt', 'r')
ZeroDivisionError Attempting division or modulo by zero. 10 / 0
AttributeError Attempting to access an attribute or method that does not exist for an object. my_list = [1, 2]
my_list.non_existent_method()
NameError Using a variable or function name that has not been defined. print(undefined_variable)
IOError General input/output error (often related to files). FileNotFoundError is a subclass of this. (Can be raised for various I/O issues, e.g., disk full, permissions)

flowchart TD

  %% Syntax Error Flow
  A1[Your Code]
  B1[/"Syntax Error (Parse Time)"/]:::error
  C1[Cannot Run]:::disabled

  A1 --> B1
  B1 -.X.-> C1

  %% Runtime Exception Flow
  A2[Your Code]
  B2[Program Runs]:::run
  C2[/"Exception (Runtime)"/]:::error

  A2 --> B2 --> C2

  %% Styling
  classDef error fill:#FECACA,stroke:#DC2626,color:#DC2626,font-weight:bold;
  classDef run fill:#D1FAE5,stroke:#059669,color:#000;
  classDef disabled fill:#E5E7EB,stroke:#9CA3AF,color:#9CA3AF;

Handling Exceptions: try...except

The fundamental structure for handling exceptions is the try...except block.

Syntax:

Python
try:
    # Code block where an exception might occur
    # (Risky operations)
    statement1
    statement2
except ExceptionType:
    # Code block to execute IF an exception of type 'ExceptionType'
    # occurs within the 'try' block
    # (Error handling code)
    handler_statement1
# Code here continues after the try...except block
# (unless the exception wasn't caught or a new one was raised)

How it works:

  1. The code inside the try block is executed.
  2. If no exception occurs during the execution of the try block, the except block is skipped, and execution continues after the try...except structure.
  3. If an exception occurs inside the try block:
    • Python immediately stops executing the rest of the try block.
    • It checks if the type of the exception raised matches the ExceptionType specified in the except clause.
    • If it matches, the code inside the except block is executed. Execution then continues after the try...except structure.
    • If it doesn’t match, Python looks for another except block in the same structure that might match, or if none match, the exception propagates upwards (potentially crashing the program if unhandled).
Python
try:
    num_str = input("Enter a number: ")
    num_int = int(num_str) # Potential ValueError
    result = 10 / num_int  # Potential ZeroDivisionError
    print(f"10 divided by your number is: {result}")
except ValueError:
    print("Invalid input. Please enter a valid integer.")
except ZeroDivisionError:
    print("Cannot divide by zero.")

print("Program continues after handling potential errors.")

flowchart TD

  Start([Start Execution])
  Try[Try Block]:::try
  Except[Except Block]:::except
  Else[Else Block]:::else
  Finally[Finally Block]:::finally
  End([Continue/End])

  Start --> Try

  %% No Exception Path
  Try -- No Exception --> Else --> Finally --> End

  %% Exception Path
  Try -. Exception! .-> Except --> Finally

  %% Styling
  classDef try fill:#ECFDF5,stroke:#10B981,color:#000,stroke-width:1.5px;
  classDef except fill:#FEF2F2,stroke:#F87171,color:#DC2626,stroke-width:1.5px;
  classDef else fill:#EFF6FF,stroke:#60A5FA,color:#000,stroke-width:1.5px;
  classDef finally fill:#F5F3FF,stroke:#A78BFA,color:#000,stroke-width:1.5px;

Handling Specific Exceptions

It’s best practice to catch specific exceptions you anticipate, rather than using a bare except: clause (which catches all exceptions, including system-exiting ones like SystemExit or KeyboardInterrupt, making it hard to debug or stop your program).

You can handle multiple specific exceptions using:

Multiple except Blocks:

Python
try:
    # Risky code
    pass # Placeholder for actual code
except ValueError:
    # Handle ValueError
    pass
except TypeError:
    # Handle TypeError
    pass
except Exception as e: # Catch any other Exception subclass
    print(f"An unexpected error occurred: {e}")

Python checks the except blocks in order and executes the first one that matches the type of the exception raised.

Tuple of Exceptions:

Python
try:
    # Risky code
    pass
except (ValueError, TypeError) as e: # Handle either ValueError or TypeError
    print(f"Input error occurred: {e}")

Accessing the Exception Object

You can capture the exception object itself using as variable_name in the except clause. This object often contains useful details about the error.

Python
try:
    file = open("non_existent_file.txt", "r")
except FileNotFoundError as error_object:
    print(f"Error accessing file: {error_object}")
    # Output might be: Error accessing file: [Errno 2] No such file or directory: 'non_existent_file.txt'

The else Block

The optional else block associated with try...except executes only if the try block completes without raising any exceptions. It’s useful for code that should run only when the “risky” operations succeed.

Syntax:

Python
try:
    # Risky operations
    result = potential_operation()
except SomeError:
    # Handle the error
    print("An error occurred.")
else:
    # Code here runs ONLY if NO exception occurred in the 'try' block
    print("Operation successful.")
    process_result(result)

The finally Block

The optional finally block executes regardless of whether an exception occurred in the try block or not. It even runs if an exception occurred and wasn’t caught, or if a return, break, or continue statement was encountered in the try or except blocks. It’s typically used for cleanup actions that must happen no matter what (e.g., closing files (though with is better for files), releasing network connections, closing database cursors).

Syntax:

Python
try:
    # Risky operations
    pass
except SomeError:
    # Handle error
    pass
else:
    # Runs if no exception
    pass
finally:
    # This code ALWAYS runs (cleanup)
    print("Executing cleanup actions.")

Flow Control Summary:

graph TD
    A[Start] --> B(Execute 'try' block);
    B -- No Exception Occurs --> C{Is there an 'else' block?};
    B -- Exception Occurs --> D{Is there a matching 'except' block?};
    
    C -- Yes --> E(Execute 'else' block);
    C -- No --> F{Is there a 'finally' block?};
    
    D -- Yes --> G(Execute matching 'except' block);
    D -- No --> H(Exception Propagates Upwards);

    E --> F;
    G --> F;
    
    F -- Yes --> I(Execute 'finally' block);
    F -- No --> J[Continue after block / Propagate];

    I --> J;
    H --> F;  


    style A fill:#f9f,stroke:#333,stroke-width:2px
    style J fill:#f9f,stroke:#333,stroke-width:2px
    style H fill:#f99,stroke:#f00,stroke-width:2px
    style D fill:#ffcc99,stroke:#333,stroke-width:1px
    style C fill:#ccffcc,stroke:#333,stroke-width:1px
    style F fill:#ccffff,stroke:#333,stroke-width:1px
    style G fill:#ffddcc,stroke:#333,stroke-width:1px
    style E fill:#ddffdd,stroke:#333,stroke-width:1px
    style I fill:#ddffff,stroke:#333,stroke-width:1px
  1. try block executes.
  2. If exception occurs: Matching except block runs. Then finally runs.
  3. If no exception occurs: else block runs (if present). Then finally runs.
  4. If exception occurs and no matching except: finally runs, then exception propagates up.

Raising Exceptions (raise)

You can intentionally raise an exception using the raise statement. This is useful when you detect an error condition in your own code (e.g., invalid input that isn’t covered by built-in exceptions) or when you want to re-raise an exception after catching it (perhaps after logging it).

Syntax:

Python
raise ExceptionType("Optional error message")
```python
def calculate_sqrt(number):
    """Calculates square root, raising ValueError for negative input."""
    if number < 0:
        raise ValueError("Input cannot be negative for square root.")
    return number ** 0.5

try:
    print(calculate_sqrt(25))
    print(calculate_sqrt(-4))
except ValueError as e:
    print(f"Error: {e}")

# Output:
# 5.0
# Error: Input cannot be negative for square root.

Custom Exceptions (Brief Mention)

For more complex applications, you can define your own custom exception classes by inheriting from Python’s built-in Exception class or one of its subclasses. This allows you to create specific error types relevant to your application’s domain. Creating custom exceptions is a more advanced topic typically covered later.

Code Examples

Example 1: Handling Input Errors

Python
# safe_division.py

def get_float_input(prompt):
    """Safely gets float input from the user, handling errors."""
    while True: # Loop until valid input is received
        try:
            value_str = input(prompt)
            value_float = float(value_str) # Potential ValueError
            return value_float
        except ValueError:
            print("Invalid input. Please enter a number (e.g., 10 or 3.14).")

def divide_numbers():
    """Performs division, handling potential errors."""
    print("Enter two numbers to divide.")
    num1 = get_float_input("Enter the numerator: ")
    num2 = get_float_input("Enter the denominator: ")

    try:
        result = num1 / num2 # Potential ZeroDivisionError
    except ZeroDivisionError:
        print("Error: Cannot divide by zero.")
    else:
        # This runs only if division was successful (no ZeroDivisionError)
        print(f"{num1} / {num2} = {result}")
    finally:
        # This runs whether division succeeded or failed
        print("Division attempt finished.")

# --- Run the division ---
divide_numbers()

Explanation:

  • get_float_input uses a while True loop and try...except ValueError to repeatedly ask for input until a valid float is entered.
  • divide_numbers calls get_float_input twice.
  • It then uses try...except ZeroDivisionError...else...finally to handle the division:
    • try: Attempts the division.
    • except: Catches division by zero.
    • else: Prints the result if division succeeded.
    • finally: Prints a message indicating the attempt is complete, regardless of outcome.

Example 2: File Handling with Exceptions

Python
# file_processor.py

def process_file(filepath):
    """Reads numbers from a file (one per line) and calculates their sum.

    Args:
        filepath (str): The path to the file.

    Returns:
        float: The sum of the numbers, or None if an error occurs.
    """
    total = 0.0
    line_num = 0
    print(f"\n--- Processing file: {filepath} ---")
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            for line in f:
                line_num += 1
                try:
                    # Attempt to convert line (stripped of whitespace) to float
                    number = float(line.strip())
                    total += number
                except ValueError:
                    # Handle error if a line is not a valid number
                    print(f"Warning: Skipping invalid number on line {line_num}: '{line.strip()}'")

    except FileNotFoundError:
        print(f"Error: File '{filepath}' not found.")
        return None # Indicate failure by returning None
    except IOError as e:
        print(f"Error reading file '{filepath}': {e}")
        return None
    except Exception as e: # Catch any other unexpected errors
        print(f"An unexpected error occurred: {e}")
        return None
    else:
        # Runs only if the file was opened and read without IO/FileNotFound errors
        print("File processed successfully.")
        return total
    finally:
        # Runs regardless of success or failure within the outer try
        print("File processing attempt concluded.")


# --- Create dummy files and run ---
try:
    with open("numbers.txt", "w") as f:
        f.write("10\n")
        f.write("20.5\n")
        f.write("invalid\n") # Add an invalid line
        f.write("30\n")
    with open("empty.txt", "w") as f:
        pass # Create an empty file
except IOError:
    print("Error creating dummy files.")


sum1 = process_file("numbers.txt")
if sum1 is not None:
    print(f"Sum from numbers.txt: {sum1}") # Expected: 60.5

sum2 = process_file("non_existent.txt")
if sum2 is not None:
    print(f"Sum from non_existent.txt: {sum2}")

sum3 = process_file("empty.txt")
if sum3 is not None:
    print(f"Sum from empty.txt: {sum3}") # Expected: 0.0

Explanation:

  • The process_file function uses nested try...except blocks.
  • The outer try handles opening the file (FileNotFoundError, IOError).
  • The inner try handles converting each line to a float (ValueError).
  • The else block associated with the outer try runs only if the file was successfully opened and processed without I/O errors (though ValueError might have occurred inside).
  • The finally block ensures a concluding message is always printed.
  • The function returns the calculated total on success or None if a major error (like file not found) occurred.

Common Mistakes or Pitfalls

  • Bare except:: Catching all exceptions hides errors and makes debugging difficult. Catch specific exceptions whenever possible.
  • Incorrect Exception Type: Catching the wrong type of exception means the actual error won’t be handled (e.g., catching TypeError when a ValueError occurs).
  • Order of except Blocks: If catching related exceptions (subclasses and superclasses), catch the more specific subclass before the more general superclass. Otherwise, the superclass handler will always execute first.
  • Hiding Bugs: Using except: with a pass statement can silently ignore errors, making it seem like code is working when it’s actually failing. Only ignore exceptions if you are certain it’s safe and intended.
  • Placing Too Much Code in try: Only include the specific lines of code that might actually raise the exception you want to handle within the try block. Code that doesn’t need error handling can often go before the try or in the else block.

Chapter Summary

Concept Description Syntax / Example
Syntax Error Error in code structure violating Python rules. Detected *before* execution. Must be fixed to run the program. if x > 5 # Missing colon
Exception (Runtime Error) Error occurring *during* program execution due to unexpected conditions (e.g., invalid operation, missing resource). 10 / 0 # ZeroDivisionError
Common Exceptions Built-in errors like TypeError, ValueError, IndexError, KeyError, FileNotFoundError, ZeroDivisionError. int('abc') # ValueError
try...except The primary mechanism to handle exceptions. Executes code in the try block; if an exception occurs, runs the matching except block. try:
risky_code()
except ValueError:
print("Handled!")
Specific Exceptions Catching particular exception types (e.g., except ValueError:) is preferred over a bare except: for clarity and safety. except FileNotFoundError:
except (TypeError, ValueError):
Accessing Exception Object Use as variable in the except clause to get details about the specific error instance. except Exception as e:
print(f"Error: {e}")
else Block Optional block associated with try...except. Executes *only if* the try block completes without raising any exceptions. try: ...
except: ...
else:
# Runs on success
finally Block Optional block. Executes *always*, regardless of whether an exception occurred, was handled, or if return/break/continue was used. Used for cleanup. try: ...
except: ...
finally:
# Always runs
raise Statement Intentionally triggers an exception. Used for custom error conditions or re-raising caught exceptions. if x < 0:
raise ValueError("Negative value")
  • Exceptions are errors detected during program execution. Unhandled exceptions crash the program.
  • Use try...except blocks to handle potential exceptions gracefully.
  • Catch specific exceptions (except ValueError:, except FileNotFoundError:) rather than using a bare except:.
  • Multiple exceptions can be handled with multiple except blocks or a tuple (except (TypeError, ValueError):).
  • Use as variable to access the exception object (except ValueError as e:).
  • The else block runs only if the try block completes without raising an exception.
  • The finally block always runs, used for essential cleanup (though with is preferred for file handling).
  • Use raise ExceptionType("message") to trigger exceptions intentionally.

Exercises & Mini Projects

Exercises

  1. Zero Division Handling: Write a function that takes two numbers as input and returns their division. Use a try...except block to catch ZeroDivisionError and return None (or print an error and return None) if division by zero occurs.
  2. List Index Handling: Create a list my_list = [1, 2, 3]. Ask the user for an index. Use try...except IndexError to try and print the element at that index. If the index is invalid, print an error message.
  3. Dictionary Key Handling: Create a dictionary my_dict = {"a": 1, "b": 2}. Ask the user for a key. Use try...except KeyError to try and print the value associated with that key. If the key doesn’t exist, print an error message.
  4. try...else: Modify the list index handling exercise (Exercise 2) to use an else block. The else block should print “Access successful!” only if the index was valid and the element was printed without error.
  5. raise ValueError: Write a function validate_age(age) that takes an integer age. If the age is less than 0 or greater than 120, raise a ValueError with an appropriate message. Otherwise, print “Age is valid.” Call this function with valid and invalid ages, wrapping the calls in a try...except ValueError block.

Mini Project: Robust File Reader

Goal: Create a script that attempts to read and print the contents of a file specified by the user, handling potential errors gracefully.

Steps:

  1. Write a Python script robust_reader.py.
  2. Ask the user to enter the name of a file they want to read using input().
  3. Use a try...except...finally block to handle the file reading:
    • try block:
      • Use a with open(...) statement inside the try block to open the user-specified file in read mode ('r') with encoding='utf-8'.
      • Read the entire content of the file using .read().
      • Print the content to the console.
      • After printing, print a message like “File reading completed successfully.” (This helps distinguish success from just the finally block running).
    • except FileNotFoundError as e: block:
      • Print an informative error message indicating the file was not found, possibly including the error object e.
    • except IOError as e: block:
      • Print an informative error message for other potential I/O errors (e.g., permission denied), including e.
    • except UnicodeDecodeError as e: block:
      • Print an error message indicating an encoding issue, suggesting the file might not be UTF-8, including e.
    • except Exception as e: block:
      • Catch any other unexpected exceptions and print a generic error message including e.
    • finally block:
      • Print a message like “Attempted to read file ‘[filename]’. Process finished.” This message should appear regardless of whether reading was successful or an error occurred.
  4. Test your script by providing names of files that exist, files that don’t exist, and potentially files with different encodings (if possible) or restricted permissions to see the different error handlers work.

Additional Sources:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top