Python Exception Handling: The Complete Beginner's Guide

This chapter teaches robust error handling with \`try/except/else/finally\`, \`raise\`, custom exceptions, chaining, and practical patterns so programs fail gracefully.

Chapter 15 of 20 · Intermediate · 40 min · Python Programming Course

Every real program eventually encounters invalid input, missing files, network failures, or unexpected states. Exception handling lets your code respond gracefully instead of crashing.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Understand what exceptions are and why they happen
  • Use try, except, else, and finally correctly
  • Catch specific exceptions and avoid broad catches
  • Access exception objects using as
  • Raise exceptions intentionally with raise
  • Define custom exception classes
  • Chain exceptions with raise ... from
  • Apply practical exception-handling patterns
  • Decide when to handle and when to propagate
  • Avoid common exception-handling mistakes

Estimated time: 40 minutes reading + 20 minutes practice

What Is an Exception?

An exception is an object raised when execution cannot continue normally. Python searches for a matching handler up the call stack.

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")

Exception Hierarchy

BaseException
├── SystemExit
├── KeyboardInterrupt
└── Exception
    ├── ValueError
    ├── TypeError
    ├── NameError
    ├── AttributeError
    ├── IndexError
    ├── KeyError
    ├── FileNotFoundError
    ├── PermissionError
    └── OSError

Catch specific exceptions whenever possible.

try/except/else/finally

try:
    value = int(input("Enter a number: "))
    result = 100 / value
except ValueError:
    print("Not a valid integer")
except ZeroDivisionError:
    print("Division by zero is not allowed")
else:
    print(f"Result: {result}")
finally:
    print("Done")
  • else runs only on success.
  • finally always runs (great for cleanup).

Catching Multiple Exceptions

def to_int(value, default=None):
    try:
        return int(value)
    except (ValueError, TypeError):
        return default

Accessing Exception Details

try:
    with open("config.json") as f:
        data = f.read()
except FileNotFoundError as e:
    print(f"Missing file: {e.filename}")

Raising Exceptions

def set_age(age):
    if not isinstance(age, int):
        raise TypeError("Age must be an integer")
    if age < 0:
        raise ValueError("Age cannot be negative")

Re-raising and Chaining

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError("Configuration load failed") from e

Use bare raise inside an except block when you need to log and propagate unchanged.

Custom Exceptions

class RegistrationError(Exception):
    pass

class StudentNotFoundError(RegistrationError):
    def __init__(self, student_id):
        self.student_id = student_id
        super().__init__(f"Student not found: {student_id}")

Custom exceptions make domain errors explicit and easier to handle.

Practical Patterns

Input validation loop

def get_valid_integer(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("Please enter a whole number")

Retry logic

import time

def with_retry(fn, attempts=3):
    last = None
    for i in range(attempts):
        try:
            return fn()
        except Exception as e:
            last = e
            if i < attempts - 1:
                time.sleep(0.5 * (2 ** i))
    raise last

A Complete Working Program

Student registration system with custom exceptions:

class RegistrationError(Exception):
    pass

class StudentNotFoundError(RegistrationError):
    def __init__(self, student_id):
        self.student_id = student_id
        super().__init__(f"Student not found: {student_id}")

class AlreadyEnrolledError(RegistrationError):
    pass

class RegistrationSystem:
    def __init__(self):
        self.students = {}

    def add_student(self, sid, name):
        if sid in self.students:
            raise RegistrationError(f"Student ID {sid} already exists")
        self.students[sid] = {"name": name, "courses": []}
        return f"{name} added"

    def enroll(self, sid, course):
        if sid not in self.students:
            raise StudentNotFoundError(sid)
        student = self.students[sid]
        if course in student["courses"]:
            raise AlreadyEnrolledError(f"{student['name']} already in {course}")
        student["courses"].append(course)
        return f"{student['name']} enrolled in {course}"

5 Exception Handling Mistakes

  1. Using bare except:
  2. Swallowing errors silently with pass
  3. Putting too much code inside one try block
  4. Using exceptions for normal control flow
  5. Losing context instead of using raise ... from

Practice Exercises

  • Date parser with robust validation
  • Safe CSV processor with row-level error reporting
  • Custom bank exception hierarchy
  • Retry decorator implementation
  • Safe execution wrapper with structured results

-> See all Python practice exercises with solutions

What Comes Next - Day 16: Advanced Data Structures

Next you will focus on efficient structures:

  • collections.deque
  • collections.Counter
  • collections.defaultdict
  • named tuples
  • heaps with heapq
  • choosing the right structure for performance

-> Continue to Day 16: Advanced Data Structures

Frequently Asked Questions

What is exception handling in Python?

Exception handling is the mechanism for managing runtime errors without crashing a program.

Difference between try, except, else, and finally?

try holds risky code, except handles errors, else runs on success, and finally always runs.

Difference between raise and raise ... from?

raise signals an error; raise ... from preserves causal context between exceptions.

Should I use bare except?

Almost never. Catch specific exceptions to avoid masking bugs and control-flow signals.

What are custom exceptions?

Application-specific exception classes representing meaningful domain errors.

What is exception chaining?

Linking a new exception to the original cause using raise NewError(...) from e.

How do I decide what to catch?

Catch only exceptions you can meaningfully recover from.

Difference between errors and exceptions?

In practice, both are exception objects; “error” is usually a conceptual label.

Chapter navigation

Frequently asked questions: Exception handling

What is exception handling in Python?

Exception handling is Python’s mechanism to catch runtime errors and respond gracefully instead of crashing.

What is the difference between try, except, else, and finally?

try contains risky code, except handles matching errors, else runs only if no error occurred, and finally always runs.

What is the difference between raise and raise ... from?

raise signals an exception; raise ... from links a new exception to an original cause to preserve debugging context.

When should I use a bare except clause?

Almost never. Bare except catches too much, including interrupts and system-exit signals.

What are custom exceptions in Python?

Custom exceptions are application-specific classes derived from Exception to model domain-specific error conditions.

What is exception chaining in Python?

Exception chaining connects related failures using raise NewError(...) from original_error.

How do I know which exceptions to catch?

Catch the most specific exceptions you can recover from and let others propagate.

What is the difference between errors and exceptions in Python?

Practically all runtime errors are represented as exception objects; the distinction is mostly conceptual.