Exception Handling — Dealing with Errors Gracefully

Even the most carefully written programs can run into unexpected problems — a missing file, a network failure, or invalid user input. **Exception handling** is Python’s way to respond to these issues gracefully, keeping your program from crashing and allowing it to recover or fail politely.

Chapter 2: Control Structures and Functions

Sub-chapter: Exception Handling — Dealing with Errors Gracefully

Even the most carefully written programs can run into unexpected problems — a missing file, a network failure, or invalid user input. Exception handling is Python’s way to respond to these issues gracefully, keeping your program from crashing and allowing it to recover or fail politely.


🧠 What Are Exceptions?

An exception is an event that disrupts the normal flow of execution.
In Python, when something goes wrong (like dividing by zero), the interpreter raises an exception — effectively saying “I don’t know how to handle this.”

There are three main types of problems in code:

TypeDescriptionExample
Syntax ErrorMistake in the structure of the code, caught before execution.if x = 5:
Runtime Error (Exception)Error during program execution.10 / 0
Logical ErrorCode runs but gives incorrect result.Using + instead of *

⚙️ Python Exception Hierarchy (Simplified)

BaseException
 ├── Exception
 │    ├── ArithmeticError
 │    │    ├── ZeroDivisionError
 │    │    └── OverflowError
 │    ├── ValueError
 │    ├── TypeError
 │    ├── FileNotFoundError
 │    ├── IndexError
 │    ├── KeyError
 │    └── RuntimeError
 └── SystemExit, KeyboardInterrupt (special cases)

💡 Most of the time, you’ll be working with the Exception branch of this hierarchy.


🧩 Handling Exceptions with try and except

Python provides the tryexcept structure to handle errors safely.

Syntax:

try:
    # Code that might raise an exception
except ExceptionType:
    # Code to handle that exception

Example — Handling division by zero:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")

Output:

Error: Division by zero is not allowed.

⚙️ Handling Multiple Exceptions

You can handle different types of errors with separate except blocks.

try:
    number = int(input("Enter a number: "))
    result = 10 / number
except ValueError:
    print("Error: Please enter a valid integer.")
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")

Or, handle multiple exceptions in one block:

except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")

🔄 Using else and finally

Example:

try:
    file = open("data.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("File not found.")
else:
    print("File read successfully!")
finally:
    print("Closing file...")
    file.close()

Output:

File not found.
Closing file...

✅ Use finally for cleanup tasks like closing files, releasing network connections, or freeing resources.


🚨 Raising Exceptions Manually

You can raise exceptions intentionally using the raise keyword — for example, to enforce rules or validate inputs.

def validate_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    print("Age is valid!")

Example use:

try:
    validate_age(-5)
except ValueError as e:
    print(f"Validation failed: {e}")

Output:

Validation failed: Age cannot be negative

⚙️ Custom Exception Classes

You can define your own exception types by subclassing Exception.
This helps create more meaningful and domain-specific error handling.

class InvalidEmailError(Exception):
    """Raised when an email address is not valid."""
    pass

def validate_email(email):
    if "@" not in email:
        raise InvalidEmailError("Email must contain '@' symbol.")
    print("Valid email!")

try:
    validate_email("example.com")
except InvalidEmailError as e:
    print("Error:", e)

Output:

Error: Email must contain '@' symbol.

🔗 Exception Chaining (raise ... from)

Sometimes, you want to raise a new exception but preserve the original context.

try:
    value = int("abc")
except ValueError as e:
    raise RuntimeError("Failed to convert string to int") from e

This makes debugging easier by preserving the original traceback.


🧰 Common Built‑in Exceptions

ExceptionDescription
ZeroDivisionErrorRaised when dividing by zero
ValueErrorRaised when an operation receives the wrong value type
TypeErrorRaised when an operation is applied to the wrong data type
FileNotFoundErrorRaised when a file is not found
IndexErrorRaised when accessing an invalid list index
KeyErrorRaised when a dictionary key is missing
ImportErrorRaised when an import fails
RuntimeErrorGeneric error for unexpected runtime issues
AttributeErrorRaised when accessing a missing attribute
OSErrorRaised for system-level errors (files, I/O, etc.)

🌐 Real-World Examples

Example 1 — Handling Invalid User Input

try:
    temp = float(input("Enter temperature: "))
except ValueError:
    print("Please enter a valid number!")

Example 2 — File Operations

try:
    with open("config.txt") as f:
        data = f.read()
except FileNotFoundError:
    print("Configuration file missing. Creating a new one...")
    open("config.txt", "w").write("default=true")

Example 3 — Network Operation

import requests

try:
    response = requests.get("https://example.com")
    response.raise_for_status()
except requests.exceptions.RequestException as e:
    print("Network error:", e)

✅ Best Practices for Exception Handling


🧾 Key Takeaways


With proper exception handling, your programs become resilient, user-friendly, and reliable — capable of recovering from errors rather than collapsing under them.