Module 6 ยท Lesson 19

Exceptions and Error Handling

๐Ÿ Pythonโฑ 14 min read๐Ÿ“– Core Concept

Exceptions: Handling the Unexpected

Exceptions are Python's way of signaling that something went wrong. Rather than returning error codes, Python raises exception objects that propagate up the call stack until caught or crash the program.

# Basic try/except try: result = 10 / 0 except ZeroDivisionError: print("Cannot divide by zero!") # Catch multiple exception types try: num = int(input("Enter a number: ")) result = 10 / num except ValueError: print("That's not a valid number") except ZeroDivisionError: print("Cannot divide by zero") # Catch multiple in one clause try: data = int("abc") except (ValueError, TypeError) as e: print(f"Conversion error: {e}") # else โ€” runs if NO exception occurred try: result = 10 / 2 except ZeroDivisionError: print("Error!") else: print(f"Success: {result}") # Runs if no exception # finally โ€” ALWAYS runs, exception or not try: f = open("file.txt") data = f.read() except FileNotFoundError: data = "" finally: # Cleanup โ€” though 'with' is better for files print("Done")

Exception Hierarchy

# Common built-in exceptions # BaseException # Exception # ValueError โ€” wrong value type # TypeError โ€” wrong type # KeyError โ€” dict key not found # IndexError โ€” list index out of range # AttributeError โ€” object doesn't have attribute # NameError โ€” variable not defined # FileNotFoundError โ€” file doesn't exist (IOError subclass) # ImportError โ€” module can't be imported # ZeroDivisionError โ€” division by zero # RuntimeError โ€” catch-all runtime errors # Catching Exception catches most exceptions (but not SystemExit, KeyboardInterrupt) try: risky_operation() except Exception as e: print(f"Something went wrong: {type(e).__name__}: {e}")

Raising Exceptions

def divide(a, b): if b == 0: raise ZeroDivisionError("Second argument cannot be zero") return a / b def set_age(age): if not isinstance(age, int): raise TypeError(f"Age must be int, got {type(age).__name__}") if age < 0 or age > 150: raise ValueError(f"Age {age} is not realistic") return age # Re-raise exception after logging try: risky_operation() except Exception as e: log_error(e) raise # Re-raises the same exception, preserving traceback # Raise from another exception (exception chaining) try: raw = open("config.json").read() except FileNotFoundError as e: raise RuntimeError("Configuration not found") from e

Custom Exceptions

class AppError(Exception): """Base exception for this application.""" pass class ValidationError(AppError): """Raised when input data is invalid.""" def __init__(self, field, message): self.field = field self.message = message super().__init__(f"Validation error on '{field}': {message}") class DatabaseError(AppError): """Raised when database operations fail.""" pass # Using custom exceptions def validate_email(email): if "@" not in email: raise ValidationError("email", "Must contain @") if "." not in email.split("@")[1]: raise ValidationError("email", "Invalid domain") return email try: validate_email("not-an-email") except ValidationError as e: print(f"Field: {e.field}, Error: {e.message}") except AppError as e: print(f"Application error: {e}")

Exception Best Practices

# GOOD: catch specific exceptions try: value = int(user_input) except ValueError: value = 0 # BAD: bare except catches everything including KeyboardInterrupt try: something() except: # Never do this pass # BAD: too broad try: something() except Exception: pass # Silently swallowing errors is rarely correct # GOOD: EAFP style (Easier to Ask Forgiveness than Permission) try: result = my_dict[key] except KeyError: result = default_value # vs LBYL style (Look Before You Leap) โ€” also fine if key in my_dict: result = my_dict[key] else: result = default_value

Key Takeaways

Practice Exercises

  1. Write a safe safe_divide(a, b) that returns None on error and logs which error occurred.
  2. Create a custom exception hierarchy for a web app: AppError, AuthError, NotFoundError, ValidationError.
  3. Write a function that reads a JSON config file. Raise a clear ConfigError if the file is missing, malformed, or missing required keys.
  4. Write a retry_on_failure(func, max_attempts, exceptions) function that retries a function on specified exceptions.
โ† File I/O