Python

Modules Data And Errors

Exception Handling

An exception is Python's way of signaling that something went wrong during execution — an error that, if unhandled, stops your program immediately.

JrCodex·6 min read

Jr Codex Python Notes

Level: Intermediate Prerequisites: Chapter 3 Time to complete: ~25 minutes


Table of Contents

  1. What is an Exception?
  2. try / except — The Basics
  3. Catching Specific Exceptions
  4. The Exception Hierarchy
  5. else and finally
  6. Accessing Exception Details
  7. Raising Exceptions Yourself
  8. Common Pitfalls
  9. Summary & Next Steps

1. What is an Exception?

An exception is Python's way of signaling that something went wrong during execution — an error that, if unhandled, stops your program immediately.

numbers = [1, 2, 3]
print(numbers[10])     # IndexError: list index out of range
print("this line never runs — the program already crashed")
Uncaught Exception → Program Crashes
─────────────────────────────────────────
Traceback (most recent call last):
  File "script.py", line 2, in <module>
    print(numbers[10])
IndexError: list index out of range
─────────────────────────────────────────

Exception handling lets you anticipate what might go wrong and respond gracefully instead of crashing.


2. try / except — The Basics

try:
    numbers = [1, 2, 3]
    print(numbers[10])
except IndexError:
    print("That index doesn't exist!")
 
print("Program continues running")     # ✓ this DOES run now
try/except Flow
─────────────────────────────────────────
  try:
      risky_code()        ← if this raises an exception...
  except SomeError:
      handle_it()           ← ...control jumps here instead of crashing
─────────────────────────────────────────
# Real-world example: user input that might not be a valid number
def get_age():
    user_input = input("Enter your age: ")
    try:
        age = int(user_input)
        return age
    except ValueError:
        print("That's not a valid number!")
        return None

3. Catching Specific Exceptions

Always catch the most specific exception type you expect — a bare except: (or except Exception:) hides bugs you didn't anticipate, making debugging much harder.

def divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("Cannot divide by zero!")
        return None
    except TypeError:
        print("Both arguments must be numbers!")
        return None
 
print(divide(10, 0))        # "Cannot divide by zero!" → None
print(divide(10, "2"))        # "Both arguments must be numbers!" → None
# Catching multiple exception types with one handler
try:
    value = int(input("Enter a number: "))
    result = 100 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Invalid input: {e}")

Why not just use bare except:?

# Dangerous — catches EVERYTHING, including typos and KeyboardInterrupt (Ctrl+C)!
try:
    resutl = compute_something()   # typo: NameError
except:
    print("Something went wrong")    # you'll never see the real bug — a NameError got silently swallowed

4. The Exception Hierarchy

Python exceptions form a class hierarchy (a preview of inheritance, covered fully in Module 4) — except matches the named class and any of its subclasses.

BaseException
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── IndexError        (list index out of range)
      │    └── KeyError          (missing dict key)
      ├── ValueError             (right type, invalid value — e.g. int("abc"))
      ├── TypeError               (wrong type entirely)
      ├── FileNotFoundError
      ├── AttributeError          (object has no such attribute/method)
      └── ...
try:
    result = 10 / 0
except ArithmeticError:            # ZeroDivisionError IS-A ArithmeticError — this catches it
    print("Some arithmetic error occurred")
 
try:
    my_dict = {"a": 1}
    print(my_dict["b"])
except Exception:                    # catches almost anything — usually too broad, but shown for illustration
    print("Some exception occurred")

Order matters — put more specific exceptions before more general ones, or the general one will swallow everything first:

try:
    risky_operation()
except Exception:            # ✗ this catches EVERYTHING first
    print("Generic error")
except ValueError:              # ✗ unreachable — Exception already caught it above!
    print("Specific error")

5. else and finally

def read_config(filename):
    try:
        with open(filename, "r") as file:
            data = file.read()
    except FileNotFoundError:
        print(f"{filename} not found, using defaults")
        data = "{}"
    else:
        print("File read successfully")     # runs ONLY if no exception occurred
    finally:
        print("Done attempting to read config")  # ALWAYS runs — success, failure, or return
 
    return data
try / except / else / finally
─────────────────────────────────────────
  try:      → code that might fail
  except:   → runs ONLY if an exception occurred
  else:     → runs ONLY if NO exception occurred
  finally:  → ALWAYS runs, no matter what (cleanup code)
─────────────────────────────────────────

finally is the standard place for cleanup that must happen regardless of success or failure — closing a network connection, releasing a lock, etc. (though with, from Chapter 2, already handles this automatically for files).


6. Accessing Exception Details

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error message: {e}")             # "division by zero"
    print(f"Error type: {type(e).__name__}")   # "ZeroDivisionError"
 
try:
    data = {"name": "Alice"}
    print(data["age"])
except KeyError as e:
    print(f"Missing key: {e}")                  # "'age'"  — the missing key itself

7. Raising Exceptions Yourself

Use raise to signal an error condition in your own code — you don't have to wait for Python to raise one naturally.

def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("Withdrawal amount must be positive")
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount
 
try:
    withdraw(100, -50)
except ValueError as e:
    print(f"Transaction failed: {e}")     # "Transaction failed: Withdrawal amount must be positive"

Re-raising an Exception

Sometimes you want to log or react to an error, then let it propagate anyway:

def process_data(data):
    try:
        return 100 / data
    except ZeroDivisionError:
        print("Logging: division by zero occurred")
        raise                              # re-raises the SAME exception, preserving the original traceback

8. Common Pitfalls

# Pitfall 1: catching Exception too broadly, hiding real bugs
try:
    result = complex_calculation()
except Exception:              # swallows EVERYTHING, including typos and logic errors
    result = 0
 
# Better — catch what you actually expect
try:
    result = complex_calculation()
except (ValueError, ZeroDivisionError):
    result = 0
 
 
# Pitfall 2: using exceptions for normal control flow (usually a code smell)
try:
    value = my_dict["key"]
except KeyError:
    value = None
# Better here — .get() is clearer AND avoids the overhead of an exception:
value = my_dict.get("key")
 
 
# Pitfall 3: silently swallowing errors with `pass`
try:
    risky_operation()
except Exception:
    pass          # ✗ error vanishes with NO trace — a debugging nightmare later

9. Summary & Next Steps

Key Takeaways

  • try/except catches exceptions so your program can recover instead of crashing; always catch the specific exception type you expect.
  • Python's exceptions form a class hierarchy — except Exception: catches almost everything, which is usually too broad for real code.
  • else runs only on success; finally always runs — useful for guaranteed cleanup.
  • Use raise to signal your own error conditions; use bare raise (no argument) inside an except block to re-raise the original exception after logging/reacting to it.

Concept Check

  1. Why is a bare except: considered bad practice?
  2. What's the difference between the else and finally blocks in a try statement?
  3. When should you use raise ValueError(...) versus just returning None?

Next Chapter

Chapter 5: Custom Exceptions & Debugging


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index