Python Custom Exceptions

In the last lesson, we learned about Python raise Statement. Now let’s create custom exceptions that clearly describe our program’s errors.

Let’s Talk About Making Our Own Exceptions

We know about exceptions. Python has many built-in ones. ValueError, TypeError, IndexError, KeyError, and so on.

But sometimes, none of them fit. We have a specific error for our program. And we want to raise it. With a name that makes sense for our situation.

That is what custom exceptions are for.

What Is a Custom Exception in Python?

A custom exception is an exception we define ourselves. We give it a name. And we use it like any other exception.

It is like creating our own error type. One that describes exactly what went wrong in our program.

Why Create Custom Exceptions in Python?

Here are some reasons.

First, clarity. If we raise a ValueError, it is vague. But if we raise a NegativeAgeError, anyone reading the code knows exactly what happened.

Second, we can catch our own exceptions specifically. Without catching other unrelated errors.

Third, it makes our code more organized. We can have a hierarchy of exceptions for our application.

How to Create a Custom Exception in Python

We create a custom exception by defining a class that inherits from Exception or one of its subclasses.

class MyError(Exception):
pass

That is it. A simple class. It inherits from Exception. And it does nothing else. But it is now a valid exception.

We can raise it.

raise MyError("Something went wrong")

And we can catch it.

try:
raise MyError("Something went wrong")
except MyError as e:
print(f"Caught: {e}")

Output

Caught: Something went wrong

That is the basic pattern.

Python Custom Exceptions: A Real-Life Example

Let’s say we are building a banking application. We want a specific error for insufficient funds.

class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError(f"Cannot withdraw {amount}. Balance is {balance}")
return balance - amount
try:
new_balance = withdraw(100, 150)
print(f"New balance: {new_balance}")
except InsufficientFundsError as e:
print(f"Error: {e}")

Output

Error: Cannot withdraw 150. Balance is 100

That is very clear. Anyone reading the code knows exactly what happened.

Adding More Details to the Exception in Python

We can give our exception class more features. Like storing extra information.

class InsufficientFundsError(Exception):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"Cannot withdraw {amount}. Balance is {balance}")

Now we can access the balance and amount from the exception object.

try:
raise InsufficientFundsError(100, 150)
except InsufficientFundsError as e:
print(f"Balance: {e.balance}")
print(f"Amount: {e.amount}")
print(f"Message: {e}")

Output

Balance: 100
Amount: 150
Message: Cannot withdraw 150. Balance is 100

That is very useful when we want to handle the error in a specific way.

A Hierarchy of Custom Exceptions in Python

We can create a base exception for our application. And then more specific ones that inherit from it.

class AppError(Exception):
pass
class InputError(AppError):
pass
class ProcessingError(AppError):
pass
class OutputError(AppError):
pass

Now we can catch all app errors with AppError. Or catch specific ones.

try:
raise InputError("Invalid input")
except InputError as e:
print(f"Input error: {e}")
except AppError as e:
print(f"App error: {e}")

Output

Input error: Invalid input

That is a clean way to organize exceptions.

When to Create Custom Exceptions in Python

Create custom exceptions when:

  • We have a specific error that does not fit a built-in one.
  • We want to catch our own errors separately from others.
  • We want to give more context about what went wrong.
  • We are building a library or framework.

Let’s not create custom exceptions for every little thing. We use built-in exceptions when they fit. Only create custom ones when they add value.

A Real-Life Example with a Validation System

Let’s build a simple form validator.

class ValidationError(Exception):
pass
class RequiredFieldError(ValidationError):
pass
class InvalidEmailError(ValidationError):
pass
def validate_email(email):
if not email:
raise RequiredFieldError("Email is required")
if "@" not in email:
raise InvalidEmailError(f"'{email}' is not a valid email")
return True
try:
validate_email("")
except RequiredFieldError as e:
print(f"Required: {e}")
except InvalidEmailError as e:
print(f"Invalid: {e}")
try:
validate_email("invalid")
except RequiredFieldError as e:
print(f"Required: {e}")
except InvalidEmailError as e:
print(f"Invalid: {e}")
try:
validate_email("user@example.com")
print("Email is valid")
except ValidationError as e:
print(f"Validation error: {e}")

Output

Required: Email is required
Invalid: 'invalid' is not a valid email
Email is valid

That shows how we can use different custom exceptions for different validation errors. And catch them separately.

Custom Exceptions with Multiple Inheritance in Python

We can also combine exceptions. For example, if we want our exception to be both a ValueError and a custom one.

class MyError(ValueError):
pass

Now MyError is a subclass of ValueError. So catching ValueError will also catch MyError.

try:
raise MyError("Custom error")
except ValueError as e:
print(f"Caught as ValueError: {e}")

Output

Caught as ValueError: Custom error

That can be useful when we want compatibility with existing code.

Common Mistakes with Python Custom Exceptions

Mistake 1: Forgetting to Inherit from Exception

class MyError: # Not an exception
pass
raise MyError("Error") # Error!

So what do we do? We inherit from Exception or a subclass.

class MyError(Exception):
pass

Mistake 2: Not Calling super().init()

class MyError(Exception):
def __init__(self, message):
self.message = message

This works, but the base Exception class is not initialized. So str(e) might not work as expected.

So what do we do? We call super().__init__(message).

class MyError(Exception):
def __init__(self, message):
super().__init__(message)
self.message = message

Mistake 3: Creating Too Many Custom Exceptions

Let’s not make a custom exception for every possible error. We use built-in ones when they fit. Only create custom ones when we need to catch them specifically or add context.

Mistake 4: Catching Only the Base Class

try:
raise InputError("Bad input")
except AppError as e: # Catches InputError too
print("App error")

If we catch the base class, we catch all subclasses. That might be what we want. But if we want to handle them differently, we catch the specific ones.

Next, we will learn about Python Introduction to Files and see how programs save and retrieve information.