Free GuideBeginner
35 min read
#python#exceptions#error handling#try except#custom exceptions#debugging

Error Handling and Exceptions

Comprehensive guide on Error Handling and Exceptions.

Error Handling and Exceptions

Overview#

Errors are inevitable in any real program — a file might not exist, a network call might fail, user input might be invalid. Python handles these situations through exceptions: objects that represent something going wrong, which can be caught and handled gracefully instead of crashing the program. This notebook covers the full exception-handling toolkit: try, except, else, finally, raising exceptions, building custom exception classes, exception chaining, context managers for cleanup, and practical debugging patterns.

By the end of this notebook you should be able to write code that fails predictably, communicates clearly what went wrong, and cleans up after itself no matter what.

1. What Is an Exception?#

When Python encounters an error during execution, it raises an exception. If nothing handles it, the program stops and prints a traceback.

Python 3.12 (WASM)
numbers = [1, 2, 3] print(numbers[10])
text
Traceback (most recent call last): File "example.py", line 2, in <module> print(numbers[10]) IndexError: list index out of range

The traceback tells you three things: where the error happened (file and line), what kind of error it is (IndexError), and a message describing it (list index out of range).

2. The try / except Block#

Wrap risky code in a try block, and handle failures in an except block.

Python 3.12 (WASM)
try: result = 10 / 0 except ZeroDivisionError: print("You can't divide by zero")

Catching multiple exception types#

Python 3.12 (WASM)
try: value = int(input("Enter a number: ")) result = 10 / value except ValueError: print("That wasn't a valid number") except ZeroDivisionError: print("Can't divide by zero")

You can also group exception types in a single except using a tuple:

Python 3.12 (WASM)
try: risky_operation() except (ValueError, TypeError) as e: print(f"Invalid input: {e}")

Catching the base Exception#

Python 3.12 (WASM)
try: do_something_risky() except Exception as e: print(f"Something went wrong: {e}")

Catching bare Exception (or worse, a bare except:) should be done sparingly — it can hide bugs by silently swallowing errors you didn't anticipate. Prefer catching the most specific exception type you expect.

3. Common Built-in Exceptions#

ExceptionRaised when
ValueErrorA value has the right type but an inappropriate value (e.g. int("abc"))
TypeErrorAn operation is applied to an object of an inappropriate type
KeyErrorA dictionary key is not found
IndexErrorA sequence index is out of range
AttributeErrorAn attribute or method doesn't exist on an object
FileNotFoundErrorA file operation targets a file that doesn't exist
ZeroDivisionErrorDivision or modulo by zero
ImportError / ModuleNotFoundErrorA module or name cannot be imported
StopIterationAn iterator has no more items
RuntimeErrorA generic error that doesn't fit other categories
NotImplementedErrorA method is defined but intentionally not implemented
RecursionErrorMaximum recursion depth exceeded
Python 3.12 (WASM)
d = {"a": 1} try: print(d["b"]) except KeyError as e: print(f"Missing key: {e}") lst = [1, 2, 3] try: lst.non_existent_method() except AttributeError as e: print(f"Attribute error: {e}")

4. The else Clause#

The else block runs only if the try block completes without raising an exception. It's useful for separating "code that might fail" from "code that should only run on success."

Python 3.12 (WASM)
try: number = int("42") except ValueError: print("Invalid number") else: print(f"Parsed successfully: {number}") # code here only runs if no exception occurred

5. The finally Clause#

The finally block always runs, whether or not an exception occurred — even if the exception is not caught. It's the right place for cleanup code like closing files or releasing resources.

Python 3.12 (WASM)
file = None try: file = open("data.txt", "r") contents = file.read() except FileNotFoundError: print("File not found") finally: if file: file.close() print("Cleanup complete")

Full order of execution#

Python 3.12 (WASM)
try: print("try block") raise ValueError("oops") except ValueError: print("except block") else: print("else block (skipped, exception occurred)") finally: print("finally block (always runs)")

Output:

text
try block except block finally block (always runs)

6. Raising Exceptions#

Use raise to trigger an exception deliberately, typically to enforce preconditions or signal invalid states.

Python 3.12 (WASM)
def set_age(age): if age < 0: raise ValueError("Age cannot be negative") return age set_age(-5) # raises ValueError: Age cannot be negative

Re-raising an exception#

Inside an except block, a bare raise re-raises the currently handled exception, preserving its original traceback. This is useful for logging or partial handling before letting the error propagate further.

Python 3.12 (WASM)
try: process_data() except ValueError as e: log_error(e) raise # re-raise the same exception

7. Exception Chaining#

When one exception is raised while handling another, Python automatically chains them, showing both in the traceback ("During handling of the above exception, another exception occurred"). You can also chain explicitly using raise ... from ....

Python 3.12 (WASM)
def load_config(): try: return int(config_value) except ValueError as e: raise RuntimeError("Invalid configuration") from e

Using from e makes the relationship explicit and keeps the original cause visible for debugging, rather than looking like an unrelated error.

To suppress chaining entirely (rare, but sometimes desired):

Python 3.12 (WASM)
raise RuntimeError("Invalid configuration") from None

8. Custom Exceptions#

For larger applications, defining custom exception classes makes error handling more precise and self-documenting. Custom exceptions should inherit from Exception (or a more specific built-in exception).

Python 3.12 (WASM)
class InsufficientFundsError(Exception): """Raised when an account doesn't have enough balance for a withdrawal.""" def __init__(self, balance, amount): self.balance = balance self.amount = amount super().__init__( f"Cannot withdraw {amount}: balance is only {balance}" ) class BankAccount: def __init__(self, balance=0): self.balance = balance def withdraw(self, amount): if amount > self.balance: raise InsufficientFundsError(self.balance, amount) self.balance -= amount account = BankAccount(100) try: account.withdraw(150) except InsufficientFundsError as e: print(e) print(f"Short by: {e.amount - e.balance}")

Building an exception hierarchy#

For a larger application, it often helps to define a base exception for your package, then more specific subclasses.

Python 3.12 (WASM)
class AppError(Exception): """Base exception for all application-specific errors.""" class ValidationError(AppError): """Raised when input data fails validation.""" class NotFoundError(AppError): """Raised when a requested resource doesn't exist.""" # Callers can catch broadly... try: do_something() except AppError as e: print(f"Application error: {e}") # ...or narrowly, depending on need try: do_something() except ValidationError: print("Please check your input") except NotFoundError: print("Resource not found")

9. Context Managers for Guaranteed Cleanup#

The with statement is closely related to exception handling — it guarantees that cleanup code runs even if an exception occurs inside the block, without needing an explicit finally.

Python 3.12 (WASM)
with open("data.txt", "r") as file: contents = file.read() # file is automatically closed here, even if read() raised an exception

You can write your own context manager using a class with __enter__ and __exit__, or more simply with contextlib.contextmanager:

Python 3.12 (WASM)
from contextlib import contextmanager @contextmanager def managed_resource(name): print(f"Acquiring {name}") try: yield name finally: print(f"Releasing {name}") with managed_resource("database connection") as res: print(f"Using {res}")

10. Assertions#

assert checks that a condition is true and raises AssertionError if not. Assertions are primarily meant for catching programmer errors during development and debugging — not for validating user input or handling expected runtime failures, since assertions can be stripped out when Python runs in optimized mode (python -O).

Python 3.12 (WASM)
def calculate_average(numbers): assert len(numbers) > 0, "List cannot be empty" return sum(numbers) / len(numbers)

For anything involving external input (user data, file contents, API responses), raise a proper exception (like ValueError) instead of relying on assert.

11. Logging Exceptions Properly#

Printing exceptions is fine for small scripts, but real applications should use the logging module so errors are recorded with context and severity.

Python 3.12 (WASM)
import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) try: risky_operation() except Exception: logger.exception("risky_operation failed") # logger.exception automatically includes the traceback

12. Common Pitfalls#

Catching too broadly#

Python 3.12 (WASM)
# Bad: hides real bugs, including typos and unexpected errors try: process() except: pass # Better: catch what you expect, let the rest propagate try: process() except ValueError as e: handle_invalid_value(e)

Using exceptions for normal control flow#

Exceptions should represent exceptional situations, not routine branching. Overusing them for expected outcomes (like checking whether a key exists) can hurt both readability and performance compared to a simple check.

Python 3.12 (WASM)
# Less ideal for something this common: try: value = d["key"] except KeyError: value = "default" # More direct: value = d.get("key", "default")

Swallowing exceptions silently#

Python 3.12 (WASM)
# Dangerous: failure disappears without a trace try: save_to_database(record) except Exception: pass # Better: at minimum, log it try: save_to_database(record) except Exception: logger.exception("Failed to save record") raise

Notes#

  • An exception's __str__ (what gets printed with print(e)) usually comes from the message passed to its constructor.
  • except Exception will not catch SystemExit or KeyboardInterrupt, since those inherit directly from BaseException, not Exception — this is intentional, so Ctrl+C and clean process exits aren't accidentally swallowed.
  • Multiple except clauses are checked top to bottom; put more specific exception types before more general ones.
  • finally runs even if the try or except block contains a return statement — the finally block always executes before the function actually returns.
  • As of Python 3.11, except* (exception groups) allows handling multiple unrelated exceptions raised together, primarily used in concurrent code.

Best Practices Checklist#

  • Catch specific exception types rather than bare except:
  • Use finally (or a context manager) for any cleanup that must always happen
  • Use else to separate "success-only" logic from the risky code in try
  • Raise exceptions with clear, descriptive messages
  • Use raise ... from ... when wrapping one exception in another
  • Define custom exception classes for domain-specific error conditions
  • Never use bare assert to validate untrusted external input
  • Log exceptions with full context instead of silently ignoring them
  • Avoid using exceptions for routine, expected control flow
  • Prefer with statements over manual try/finally for resource management wherever possible
Knowledge Checkpoint

Python Error Handling Checkpoint

Q1.What is the difference between the EAFP and LBYL programming philosophies in Python?
AEAFP (Easier to Ask for Forgiveness than Permission) uses try/except blocks; LBYL (Look Before You Leap) performs explicit condition checks prior to operations.
BEAFP is slower than LBYL in all scenarios.
CLBYL is the official recommended standard in PEP 8.
DEAFP only applies to file I/O.
Q2.In a `try...except...else...finally` structure, when does the `else` block execute?
AWhenever an exception is caught and handled.
BOnly when NO exception was raised in the `try` block.
CAlways, before `finally`.
DOnly if the `finally` block raises an error.
Q3.Which syntax in Python 3 allows explicit exception chaining while preserving the original traceback as `__cause__`?
Araise NewError(original_error)
Braise NewError from original_error
Craise with original_error: NewError
Dtry { raise NewError }
Track Your Learning

Finished studying this notebook?

Mark this guide as completed to update your course progress roadmap.