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 withtry...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:
- 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")
- 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] |
KeyError |
A dictionary key is not found. | my_dict = {'a': 1} |
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] |
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:
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:
- The code inside the
try
block is executed. - If no exception occurs during the execution of the
try
block, theexcept
block is skipped, and execution continues after thetry...except
structure. - 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 theexcept
clause. - If it matches, the code inside the
except
block is executed. Execution then continues after thetry...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 immediately stops executing the rest of the
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:
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:
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.
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:
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:
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
try
block executes.- If exception occurs: Matching
except
block runs. Thenfinally
runs. - If no exception occurs:
else
block runs (if present). Thenfinally
runs. - 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:
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
# 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 awhile True
loop andtry...except ValueError
to repeatedly ask for input until a valid float is entered.divide_numbers
callsget_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
# 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 nestedtry...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 outertry
runs only if the file was successfully opened and processed without I/O errors (thoughValueError
might have occurred inside). - The
finally
block ensures a concluding message is always printed. - The function returns the calculated
total
on success orNone
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 aValueError
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 apass
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 thetry
block. Code that doesn’t need error handling can often go before thetry
or in theelse
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: |
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: |
else Block |
Optional block associated with try...except . Executes *only if* the try block completes without raising any exceptions. |
try: ... |
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: ... |
raise Statement |
Intentionally triggers an exception. Used for custom error conditions or re-raising caught exceptions. | if x < 0: |
- 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 bareexcept:
. - 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 thetry
block completes without raising an exception. - The
finally
block always runs, used for essential cleanup (thoughwith
is preferred for file handling). - Use
raise ExceptionType("message")
to trigger exceptions intentionally.
Exercises & Mini Projects
Exercises
- Zero Division Handling: Write a function that takes two numbers as input and returns their division. Use a
try...except
block to catchZeroDivisionError
and returnNone
(or print an error and returnNone
) if division by zero occurs. - List Index Handling: Create a list
my_list = [1, 2, 3]
. Ask the user for an index. Usetry...except IndexError
to try and print the element at that index. If the index is invalid, print an error message. - Dictionary Key Handling: Create a dictionary
my_dict = {"a": 1, "b": 2}
. Ask the user for a key. Usetry...except KeyError
to try and print the value associated with that key. If the key doesn’t exist, print an error message. try...else
: Modify the list index handling exercise (Exercise 2) to use anelse
block. Theelse
block should print “Access successful!” only if the index was valid and the element was printed without error.raise ValueError
: Write a functionvalidate_age(age)
that takes an integerage
. If the age is less than 0 or greater than 120,raise
aValueError
with an appropriate message. Otherwise, print “Age is valid.” Call this function with valid and invalid ages, wrapping the calls in atry...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:
- Write a Python script
robust_reader.py
. - Ask the user to enter the name of a file they want to read using
input()
. - Use a
try...except...finally
block to handle the file reading:try
block:- Use a
with open(...)
statement inside thetry
block to open the user-specified file in read mode ('r'
) withencoding='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).
- Use a
except FileNotFoundError as e:
block:- Print an informative error message indicating the file was not found, possibly including the error object
e
.
- Print an informative error message indicating the file was not found, possibly including the error object
except IOError as e:
block:- Print an informative error message for other potential I/O errors (e.g., permission denied), including
e
.
- Print an informative error message for other potential I/O errors (e.g., permission denied), including
except UnicodeDecodeError as e:
block:- Print an error message indicating an encoding issue, suggesting the file might not be UTF-8, including
e
.
- Print an error message indicating an encoding issue, suggesting the file might not be UTF-8, including
except Exception as e:
block:- Catch any other unexpected exceptions and print a generic error message including
e
.
- Catch any other unexpected exceptions and print a generic error message including
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.
- 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: