Skip to content

A Comprehensive Guide to Custom Exceptions and Raising Exceptions in Python

Updated: at 05:12 AM

Python provides built-in exception classes for handling errors and other exceptional conditions in programs. However, there are cases where built-in exceptions like ValueError, TypeError or OSError do not sufficiently capture the specific problem that has occurred. In such scenarios, custom exceptions become necessary.

Custom exceptions in Python allow developers to define new exception classes that are tailored to their program’s needs. These custom exceptions clearly communicate what went wrong and why to the user. Raising exceptions with the raise statement is the standard way to trigger these custom exceptions intentionally in the code.

In this comprehensive guide, we will cover the following topics in detail with example code snippets:

Table of Contents

Open Table of Contents

Benefits of Custom Exceptions in Python

Here are some key reasons why defining custom exception classes can be useful:

Basic Syntax for Defining Custom Exceptions

Custom exceptions are defined by subclassing the built-in Exception class or another built-in exception like RuntimeError.

Here is basic syntax for defining a custom exception:

class MyCustomError(Exception):
    pass

Let’s dissect this:

Now we can raise this exception like:

raise MyCustomError

This is the basic syntax. Next we will explore custom exceptions in more detail.

Custom Exception Classes vs. Built-in Exceptions

Python has many built-in exceptions like TypeError, ValueError, OSError and so on. A natural question is when to use custom exceptions versus built-in ones.

Use custom exceptions when:

It’s recommended to subclass built-in exceptions when it closely matches the specific error. For unrelated errors, subclass the base Exception class directly.

Raising Exceptions with the raise Statement

The raise statement allows triggering exceptions explicitly in Python code. We can raise built-in or custom exceptions.

Here is basic syntax for raising exceptions:

raise ExceptionClass('Error message')

Let’s see an example with a custom exception:

class InvalidEmailError(Exception):
    pass

def validate_email(email):
    if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
        raise InvalidEmailError('Invalid email address')

We define a custom InvalidEmailError exception. Inside the validate_email() function, we check if the email matches a regex pattern. If not, we raise InvalidEmailError.

This is cleaner than just returning an error code or relying on assertions. Explicit exceptions make error handling simple in calling code:

try:
    validate_email(email)
except InvalidEmailError as e:
    print(e)

We use try-except to gracefully handle the custom exception.

The Exception Hierarchy - Inheriting from Built-in Exceptions

All built-in exceptions inherit from BaseException. This forms a hierarchy, with Exception and RuntimeError being common parent classes.

It’s considered best practice for custom exceptions to extend appropriate built-in parent exception classes.

For example:

class InvalidEmailError(ValueError):
    pass

Here InvalidEmailError inherits from ValueError, since an invalid email is related to invalid data.

This allows catching subclasses using the parent exception:

try:
   ...
except ValueError:
   # Handles InvalidEmailError too

Some common built-in parent exceptions are:

Adding Attributes and Data to Custom Exceptions

Custom exceptions can store additional context and data by defining the __init__() constructor:

class InvalidEmailError(ValueError):
    def __init__(self, message, email):
        super().__init__(message) # Initialize parent ValueError
        self.email = email # Add custom attribute

Now we can access the invalid email in the exception handler:

try:
    validate_email(email)
except InvalidEmailError as e:
    print(e.email)

This is useful for capturing state related to the exception. But be careful not to overuse it.

Handling Custom Exceptions

Code that calls functions that raise custom exceptions should handle them properly.

There are a few ways to handle exceptions in Python:

1. try/except blocks:

try:
    validate_email(email)
except InvalidEmailError:
    print("Invalid email")

2. Catching parent exceptions:

try:
   validate_email(email)
except ValueError:
   print("Invalid input")

Since InvalidEmailError inherits from ValueError, this works.

3. Catch all exceptions:

try:
    validate_email(email)
except Exception as e:
    print(e)

This is not recommended as it can conceal other issues.

4. Specifying multiple exceptions:

try:
  ...
except (ValueError, EOFError):
  ...

Best Practices for Custom Exceptions

Here are some best practices to keep in mind when defining and using custom exceptions in Python:

Adhering to these best practices will ensure your custom exceptions are implemented well.

Example Use Cases for Custom Exceptions

Let’s explore some examples of real-world use cases where custom exceptions can be very helpful:

Validating Input Data

Custom exceptions allow validating input data elegantly:

class InvalidEmailError(ValueError):
    pass

def validate_email(email):
    if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
        raise InvalidEmailError('Invalid email')

We can validate different inputs by defining different exceptions.

Handling Missing Configuration

For a configuration module, we can define custom exceptions for cases when expected config is missing:

class MissingConfigError(RuntimeError):
    pass

class Config:
    def __init__(self):
        try:
            self.load_config_file()
        except FileNotFoundError as e:
            raise MissingConfigError("Config file missing") from e

This communicates the issue more clearly compared to a generic RuntimeError.

Clarifying Database Errors

We can differentiate classes of database errors using custom exceptions:

class DuplicateKeyError(RuntimeError):
    pass

class DatabaseError(RuntimeError):
    pass

try:
    db.insert(data)
except db.DuplicateKeyException:
    raise DuplicateKeyError()
except db.DatabaseError:
    raise DatabaseError()

The custom exceptions wrap lower-level database exceptions nicely.

Parameter Validation in APIs

Custom exceptions allow validating function/method parameters elegantly:

class InvalidArgumentError(TypeError):
    pass

def send_message(destination, content):
    if not isinstance(destination, str):
        raise InvalidArgumentError("Destination must be a string")

    # Rest of logic

This clarifies expected types upfront in the docstring/code vs. letting a generic TypeError happen later.

Handling Errors Across Module Boundaries

Custom exceptions can propagate errors across module boundaries cleanly:

# module1.py
class ErrorA(Exception):
    pass

def process():
    raise ErrorA("Error A occurred")


# module2.py
from module1 import ErrorA

try:
   module1.process()
except ErrorA as e:
    print("Handling Error A")

The custom ErrorA encapsulates module-specific errors that can be handled properly in calling code.

Enforcing API Contracts

Custom exceptions allow enforcing preconditions, postconditions, and invariants in APIs:

class InvalidStateError(Exception): pass

class Parser:
    def __init__(self):
        self.state = "new"

    def process(self):
        if self.state != "new":
            raise InvalidStateError("Parser must be in 'new' state")

        # Actual processing logic

        self.state = "processed"

The custom exception InvalidStateError enforces that process() is only called on a new parser instance.

These were some common real-world examples of how custom exceptions can make error handling more robust, constraints clearer, and code organization better.

Conclusion

In this comprehensive guide, we explored various aspects of custom exceptions in Python - from basic syntax for defining them to real-world use case examples. Key takeaways include:

Defining and working with custom exceptions is an important skill for Python developers. Used properly, they make error handling and APIs more robust and Python code clearer. Hopefully this guide provides a solid foundation on the topic.