Previous | Tutorial index | Next

📚 Tutorial 3: Handling Exceptions with try...except

Learning Objectives

1. Introduction: The Art of Graceful Error Recovery

In Tutorial 2, we learned about the various built-in exceptions that Python can raise. But knowing what can go wrong is only half the battle; the other half is knowing how to respond when things do go wrong. This is where Python's exception handling mechanism—centered around the try...except block—comes into play.

Proper exception handling is what separates a program that crashes at the first sign of trouble from one that gracefully recovers, provides useful feedback, and continues operating. It is a hallmark of professional, robust software.

In this tutorial, we will explore every aspect of the try statement, including:

2. Key Terms

Before diving into the content, familiarise yourself with these key terms:

Term Definition
try block Encloses code that may raise an exception.
except block Catches and handles specific exceptions; you can have multiple.
except (Error1, Error2) Catches multiple exception types in one block using a tuple.
except ... as e Accesses the exception object for details (error message, attributes).
else clause Runs only if no exception occurs in the try block.
finally clause Always runs, regardless of exceptions; ideal for cleanup.
Bare except: Catches all exceptions, including system-exiting ones – not recommended.
Exception chaining Using raise ... from to link a new exception to the original cause.
Context manager The with statement for automatic resource cleanup (alternative to finally).

3. Real-World Analogy: The Airplane Cockpit

Imagine you are a pilot flying a commercial airliner. The flight is your program.

This analogy illustrates the structure of Python's exception handling: you anticipate problems, respond to each appropriately, perform normal tasks if all is well, and always clean up afterwards.

4. The try Block: The Starting Point

4.1 Purpose

The try block is used to enclose a section of code that you suspect might raise an exception. You are saying, "I'm going to try to execute this code, but I acknowledge that it might fail, and if it does, I have a plan."

4.2 Syntax

try: # Code that may raise an exception statement1 statement2 ...

4.3 Important Points

5. The except Block: Catching and Handling Exceptions

5.1 Purpose

The except block catches an exception that has been raised in the try block and allows you to respond to it. Without an except handler, an unhandled exception would terminate your program.

5.2 Basic Syntax

try: risky_code() except ExceptionType: # Handle the exception print("Something went wrong!")

5.3 Catching Specific Exceptions

It is best practice to catch specific exceptions rather than using a bare except:. This prevents your code from accidentally swallowing critical system exceptions like KeyboardInterrupt or SystemExit.

try: number = int(input("Enter a number: ")) result = 10 / number except ValueError: print("That's not a valid number!") except ZeroDivisionError: print("Cannot divide by zero!")

5.4 Accessing the Exception Object

You can use the as keyword to assign the exception object to a variable, allowing you to access its details (e.g., error message, arguments).

try: with open("missing.txt", "r") as file: content = file.read() except FileNotFoundError as e: print(f"File not found. Error details: {e}") print(f"Error number: {e.errno}") print(f"Error message: {e.strerror}")

5.5 Catching Multiple Exceptions in One Block

If you want to handle several exceptions in the same way, you can group them in a tuple.

try: value = int(input("Enter a number: ")) result = 100 / value print(result) except (ValueError, ZeroDivisionError) as e: print(f"Input error: {e}")

5.6 The Bare except: – Use with Caution

A bare except: catches all exceptions, including SystemExit and KeyboardInterrupt, which are generally not meant to be caught. This can make it difficult to terminate your program or to debug.

try: risky_code() except: # Bad practice print("Something went wrong")

Better to catch Exception (which excludes system-exiting exceptions) or be even more specific.

try: risky_code() except Exception as e: print(f"An error occurred: {e}")

5.7 Order of except Blocks Matters

Python checks except blocks in the order they appear. The first matching block is executed. Therefore, you should place more specific exceptions before more general ones.

try: # Some code except ZeroDivisionError: # Specific first print("Division by zero") except ArithmeticError: # More general parent print("Some arithmetic error") except Exception: # Very general print("Any other error")

If you reversed the order, the general except ArithmeticError would catch the ZeroDivisionError before the specific block could, which is probably not what you want.

6. The else Clause: Code That Runs on Success

6.1 Purpose

The else clause is executed only if no exception was raised in the try block. It allows you to separate the "successful" code from the error-handling code, making both clearer.

6.2 Syntax

try: risky_code() except SomeError: print("Error handled") else: # Runs only if no exception occurred print("All went well!")

6.3 When to Use else

6.4 Example

try: with open("data.txt", "r") as file: data = file.read() except FileNotFoundError: print("File not found, using default data.") data = "default" else: print("File read successfully.") # Process data here, knowing it's valid processed = data.upper() print(f"Processed data: {processed}")

7. The finally Clause: Code That Always Runs

7.1 Purpose

The finally clause is executed no matter what—whether an exception occurred or not, whether it was caught or not. It is used for cleanup actions that must happen regardless of the outcome.

7.2 Syntax

try: risky_code() except SomeError: print("Error occurred") finally: # Always runs print("Cleanup performed")

7.3 Common Uses

7.4 Example

file = None try: file = open("output.txt", "w") file.write("Important data") except OSError as e: print(f"Could not write to file: {e}") finally: if file is not None: file.close() # Always close the file print("File closed.")

7.5 Important Behaviour

8. Complete Structure of try...except...else...finally

You can combine all four parts in one statement. The order is fixed:

try: # Code that may raise an exception except ExceptionType1: # Handle specific exception except ExceptionType2: # Handle another exception else: # Runs if no exception finally: # Always runs

8.1 Example

def safe_divide(): try: numerator = float(input("Enter numerator: ")) denominator = float(input("Enter denominator: ")) result = numerator / denominator except ValueError: print("Please enter valid numbers.") except ZeroDivisionError: print("Cannot divide by zero.") else: print(f"The result is: {result}") finally: print("Division operation attempted.") safe_divide()

9. Advanced Patterns and Best Practices

9.1 Nested try...except Blocks

You can nest exception handlers for fine-grained control. A common pattern is to have an inner try for a specific operation and an outer try for broader recovery.

try: # Outer try for high-level handling file = open("config.txt", "r") try: data = file.read() number = int(data) except ValueError: print("Invalid integer in config file.") finally: file.close() except FileNotFoundError: print("Config file missing.")

9.2 Re-raising Exceptions

Sometimes you want to catch an exception, do some logging or cleanup, and then re-raise it to be handled at a higher level. Use raise without arguments inside an except block.

try: risky_operation() except ValueError as e: print(f"Logging: {e}") raise # Re-raise the same exception

9.3 Raising Your Own Exceptions

You can use raise to throw an exception intentionally, often after catching a low-level exception and converting it to a higher-level one.

try: with open("data.txt", "r") as f: content = f.read() except FileNotFoundError: raise RuntimeError("Data file is missing, cannot proceed.") from None

9.4 Using sys.exc_info() for Debugging

The sys.exc_info() function returns a tuple (type, value, traceback) for the current exception, useful for logging.

import sys try: 1 / 0 except: exc_type, exc_value, exc_traceback = sys.exc_info() print(f"Type: {exc_type}") print(f"Value: {exc_value}") # You can also use the traceback module to get a stack trace.

9.5 Context Managers (with) as an Alternative to finally

For resource management, the with statement (context manager) automatically handles cleanup (e.g., closing files). It is often preferred over manual try...finally for such tasks.

# Instead of: file = open("file.txt", "r") try: data = file.read() finally: file.close() # Prefer: with open("file.txt", "r") as file: data = file.read() # File is automatically closed when the with block exits.

However, with only handles resource cleanup; for other cleanup actions (like logging, restoring state), you still need try...finally.

10. Common Pitfalls and How to Avoid Them

Pitfall Solution
Catching too broad an exception (bare except:) Catch specific exceptions or at least Exception.
Catching KeyboardInterrupt unintentionally Don't use bare except:.
Swallowing exceptions silently Log the error or re-raise if you can't handle it.
Having an empty except block At least print a message or log; never leave it empty.
Incorrect order of except blocks Put more specific exceptions first.
Forgetting to clean up resources Use finally or with statements.
Overusing exceptions for flow control Exceptions are for exceptional situations; use conditionals when possible.
Catching Exception but not raising a useful message Include context in the error message.

11. Real-World Example: Robust File Processor

Let's combine all the concepts in a practical example. This function reads a list of numbers from a file, computes their average, and writes the result to another file. It handles all possible errors gracefully.

import os def process_numbers(input_file, output_file): try: # Open input file with open(input_file, 'r') as infile: lines = infile.readlines() except FileNotFoundError: print(f"Error: Input file '{input_file}' not found.") return except PermissionError: print(f"Error: Permission denied to read '{input_file}'.") return except OSError as e: print(f"Error: OS error reading '{input_file}': {e}") return numbers = [] for line_num, line in enumerate(lines, 1): line = line.strip() if not line: # skip empty lines continue try: num = float(line) numbers.append(num) except ValueError: print(f"Warning: Line {line_num} contains invalid number '{line}'. Skipping.") if not numbers: print("No valid numbers found in the input file.") return average = sum(numbers) / len(numbers) try: with open(output_file, 'w') as outfile: outfile.write(f"Average of {len(numbers)} numbers: {average:.2f}\n") except PermissionError: print(f"Error: Permission denied to write to '{output_file}'.") except OSError as e: print(f"Error: OS error writing to '{output_file}': {e}") else: print(f"Successfully wrote average to '{output_file}'.") finally: print("Processing completed.") # Example usage: process_numbers("data.txt", "average.txt")

12. Summary

Component Purpose
try Contains code that may raise exceptions.
except Catches and handles specific exceptions; you can have multiple.
except (Error1, Error2) Catches multiple exception types in one block.
except ... as e Access the exception object for details.
else Runs only if no exception occurs in the try block.
finally Always runs, regardless of exceptions; ideal for cleanup.

Best Practices:

13. Self-Assessment Quiz

Test your understanding of the concepts covered in this tutorial. Answer each question, then click to reveal the correct answer.

Q1: What is the primary purpose of the try block?

AnswerTo enclose code that might raise an exception, so it can be handled.

Q2: Which of the following is NOT a valid way to catch a specific exception?

Answer(D) `except:` is a bare `except` that catches all exceptions, not a specific type.

Q3: True or False: The else clause runs only if an exception is raised in the try block.

AnswerFalse. `else` runs only if no exception occurs in the `try` block.

Q4: What is the output of the following code?

try: print("A") x = 1 / 0 print("B") except ZeroDivisionError: print("C") else: print("D") finally: print("E")
Answer(C) A, C, E. "A" is printed, then ZeroDivisionError is raised, "B" is skipped; "C" is printed, "D" is skipped (because exception occurred); "E" is always printed.

Q5: Which of the following is the correct order of clauses in a try statement?

Answer(B) `try`, `except`, `else`, `finally` is the correct order.

Q6: What happens if an exception is raised inside a finally block?

Answer(C) The exception raised in `finally` replaces any pending exception from the `try` or `except` block.

Q7: Which of the following is a better practice than using a bare except:?

Answer(C) Catching specific exceptions is best; `except Exception:` is better than bare, but still broad. `except BaseException:` catches system-exiting exceptions too.

Q8: If you have a try block with two except blocks: one for IndexError and one for LookupError, which one should come first?

Answer(A) Specific exceptions (`IndexError`) must come before more general ones (`LookupError`).

Q9: The following code uses a return inside the try block. Will the finally block execute?

def func(): try: return 42 finally: print("Finally!")
Answer(A) Yes, `finally` always executes, even after a `return` statement.

Q10: When should you use the else clause instead of putting code after the try...except block?

Answer(B) `else` ensures the code runs only on success and is not accidentally caught by an outer `except`.

14. Practical Exercises

Complete the exercises below to reinforce your understanding. Sample solutions are provided after each exercise.

Exercise 1: Basic Exception Handling

Instructions: Write a program that asks the user for two numbers and then prints the result of dividing the first by the second. Handle the following exceptions:

Your Program Structure:

  1. Get input from the user.
  2. Convert to floats (may raise ValueError).
  3. Perform division (may raise ZeroDivisionError).
  4. Print the result if successful.
  5. Always print "Thank you for using the calculator." at the end.
Sample Answer
def divide_numbers(): try: num1 = float(input("Enter the first number: ")) num2 = float(input("Enter the second number: ")) result = num1 / num2 print(f"Result: {result}") except ValueError: print("Error: Please enter valid numbers.") except ZeroDivisionError: print("Error: Cannot divide by zero.") except Exception as e: print(f"An unexpected error occurred: {e}") finally: print("Thank you for using the calculator.") # Run it divide_numbers()

Exercise 2: Multiple Exceptions in One Block

Instructions: Write a function parse_and_square(data) that:

Test your function with:

Sample Answer
def parse_and_square(data): try: number = int(data) square = number * number return square except (ValueError, TypeError) as e: print(f"Invalid input: {e}") except Exception as e: print(f"Unexpected error: {e}") # Test print(parse_and_square("5")) # 25 print(parse_and_square("abc")) # Invalid input: ... print(parse_and_square([1, 2])) # Invalid input: ...

Exercise 3: Using else and finally

Instructions: Write a program that attempts to open a file named "config.txt", read its content, and split it into lines. If the file is not found, create a default configuration file with the content "default_config". If the file is found successfully, print the number of lines. Always print "File operation completed." regardless of the outcome.

Hints:

Sample Answer
def check_config(): try: with open("config.txt", "r") as file: lines = file.readlines() except FileNotFoundError: print("Config file not found. Creating default config.") with open("config.txt", "w") as file: file.write("default_config") lines = [] else: print(f"Config file has {len(lines)} lines.") finally: print("File operation completed.") check_config()

Exercise 4: Nested Exception Handling

Instructions: Write a function read_number_from_file(filename) that:

Sample Answer
def read_number_from_file(filename): try: with open(filename, "r") as file: line = file.readline().strip() except FileNotFoundError: print("File not found.") return None except Exception: # Re-raise any other exception raise try: number = int(line) return number except ValueError: print("Invalid integer in file.") return None

Exercise 5: Re-raising Exceptions

Instructions: Write a function safe_divide(a, b) that:

Sample Answer
def safe_divide(a, b): try: return a / b except ZeroDivisionError: print("Cannot divide by zero.") raise except Exception as e: print(f"An error occurred: {e}") raise # Main program try: result = safe_divide(10, 0) except ZeroDivisionError: print("Caught in main.")

15. Homework Questions

Short Answer Questions

1. You are building a program that processes a CSV file containing user data. The file has columns: name, age, email. Write a function process_users(filename) that:

  1. Opens the file for reading.
  2. Reads each line, skips the header (first line).
  3. For each subsequent line:
  4. Collects valid user data into a list of dictionaries.
  5. Returns the list.
  6. Handles the following exceptions:

Bonus: Use a finally block to print "Processing complete." regardless of success or failure.

Sample Answer
def process_users(filename): users = [] try: with open(filename, 'r') as file: lines = file.readlines() except FileNotFoundError: print("File not found.") return [] except PermissionError: print("Permission denied.") return [] except OSError as e: print(f"OS error occurred: {e}") return [] except Exception as e: print(f"Unexpected error: {e}") raise finally: print("Processing complete.") # Skip header for i, line in enumerate(lines[1:], start=2): parts = line.strip().split(',') if len(parts) < 3: print(f"Warning: Line {i} has insufficient columns. Skipping.") continue name, age_str, email = parts[0], parts[1], parts[2] try: age = int(age_str) except ValueError: print(f"Warning: Line {i} has invalid age '{age_str}'. Skipping.") continue if '@' not in email: print(f"Warning: Line {i} has invalid email '{email}'. Skipping.") continue users.append({"name": name, "age": age, "email": email}) return users # Test (assuming file exists) # users = process_users("users.csv")

2. Design a simple bank account class BankAccount with the following:

Write a driver program that:

Sample Answer
class BankAccount: def __init__(self, account_number, initial_balance=0): self.account_number = account_number self.balance = initial_balance def deposit(self, amount): if amount < 0: raise ValueError("Deposit amount cannot be negative.") self.balance += amount def withdraw(self, amount): if amount < 0: raise ValueError("Withdrawal amount cannot be negative.") if amount > self.balance: raise RuntimeError("Insufficient balance.") self.balance -= amount def get_balance(self): return self.balance # Driver program def main(): account = BankAccount("12345", 100) while True: cmd = input("Enter command (deposit, withdraw, balance, quit): ").strip().lower() if cmd == "quit": break elif cmd == "balance": print(f"Balance: {account.get_balance()}") elif cmd in ("deposit", "withdraw"): try: amount = float(input("Enter amount: ")) if cmd == "deposit": account.deposit(amount) else: account.withdraw(amount) print(f"New balance: {account.get_balance()}") except ValueError as e: print(f"Error: {e}") except RuntimeError as e: print(f"Error: {e}") except Exception as e: print(f"Unexpected error: {e}") else: print("Invalid command.") print("Thank you for using our banking system.") if __name__ == "__main__": main()

Essay Question

3. The following code is buggy and has poor exception handling. Identify the problems and rewrite it following best practices.

try: filename = input("Enter filename: ") with open(filename, "r") as file: data = file.read() numbers = data.split() total = 0 for n in numbers: total += int(n) print("Average:", total / len(numbers)) except: print("Error")

List the problems and provide a corrected version.

Sample Answer

Problems identified:

  1. Bare except: catches everything, including KeyboardInterrupt and SystemExit.
  2. If the file is empty or contains no numbers, len(numbers) could be zero, causing ZeroDivisionError that is caught by the bare except, hiding the true problem.
  3. The int(n) might raise ValueError if a token isn't a number; this also gets swallowed.
  4. No specific handling for FileNotFoundError or PermissionError.
  5. The error message "Error" is too vague and doesn't help the user.

Corrected version:

def process_data(): try: filename = input("Enter filename: ") try: with open(filename, "r") as file: data = file.read() except FileNotFoundError: print(f"Error: File '{filename}' not found.") return except PermissionError: print(f"Error: Permission denied to read '{filename}'.") return except OSError as e: print(f"Error: OS error reading file: {e}") return numbers = data.split() if not numbers: print("The file is empty or contains no data.") return total = 0 count = 0 for n in numbers: try: total += int(n) count += 1 except ValueError: print(f"Warning: '{n}' is not a valid integer; skipping.") if count == 0: print("No valid numbers found.") return average = total / count print(f"Average of {count} numbers: {average}") except Exception as e: print(f"An unexpected error occurred: {e}") raise # Optionally re-raise for debugging process_data()

4. Write two versions of a function write_to_file(filename, content) that writes content to a file and ensures the file is closed.

Explain which version is preferred and why.

Sample Answer

Version 1 (try...finally):

def write_to_file_v1(filename, content): file = None try: file = open(filename, "w") file.write(content) except Exception: print("Error writing to file") raise finally: if file: file.close()

Version 2 (with statement):

def write_to_file_v2(filename, content): try: with open(filename, "w") as file: file.write(content) except Exception: print("Error writing to file") raise

Explanation: Version 2 is preferred because:

Research Question

5. Discuss the role of exception handling in software development. Include the following points:

Provide examples to illustrate your points.

Sample Answer

Exception handling is a cornerstone of professional software development, serving as the primary mechanism for managing errors and unexpected conditions. Its importance cannot be overstated—users expect applications to behave predictably, and a program that crashes abruptly provides a poor experience. Graceful error recovery allows programs to handle problems like missing files, invalid user input, or network failures without terminating, instead presenting helpful error messages and allowing users to correct their actions.

The balance between catching exceptions early versus letting them propagate is a key design consideration. The principle is to catch exceptions at the level where you can meaningfully handle them. Low-level functions should not catch exceptions they cannot resolve; instead, they should let them propagate to higher levels where the appropriate recovery strategy is known. For example, a file-reading function might catch FileNotFoundError and attempt to create a default file, while a function that calls it might catch a broader RuntimeError and log it for debugging.

When it comes to using exceptions for control flow, the consensus is clear: exceptions should be used for exceptional conditions, not for normal program flow. Using exceptions to control regular logic—such as using a try...except to check if a key exists in a dictionary instead of using get() or if key in dict—is considered an antipattern. Exceptions are computationally expensive when raised, and they make the code harder to read and maintain. Conditionals are more efficient and clearer when the condition is expected.

The trade-off between using try...except and conditional checks is nuanced. For conditions that are rare and truly exceptional (e.g., file not found when it should exist), try...except is appropriate because it keeps the normal code path clean and separates error handling from business logic. For conditions that are common and expected (e.g., checking if a list is empty before accessing an element), conditionals are preferred because they are clearer and more performant. In practice, the best approach is to use conditionals for expected conditions and exceptions for unexpected ones, striking a balance that maximizes both code clarity and performance.

Examples:

This tutorial provides a comprehensive understanding of exception handling with try...except. The quizzes, exercises, and homework problems will help you master these concepts and apply them in real-world scenarios. Happy coding!

Previous | Tutorial index | Next