Python Custom Exceptions

In the last lesson you learned the raise Statement. That is where you signal a problem yourself instead of waiting for Python to do it. So far you have been raising errors that Python already gives you, like ValueError or TypeError. But those names are general. What if your problem has a name of its own, like “not enough money in the account”? You can make your own exception type for exactly that. That is what this lesson is about.

🤔 Why Make Your Own Exception?

Imagine you are writing the code for a bank app, something like a digital wallet. A customer named Alex tries to take out more money than they have. So you raise an error.

Here is the catch. The error you reach for is probably ValueError.

def withdraw(balance, amount):
if amount > balance:
raise ValueError("Not enough money")
return balance - amount

It works. But ValueError is a very general name. Python raises ValueError for a bad number, a wrong format, and many other things. So when another part of your program tries to catch this one, it cannot tell which ValueError it caught. Was it the “not enough money” one? Or some other bad value? You cannot tell them apart.

That is the pain. A general error name hides what really went wrong. The fix is a custom exception. That is your own error type with a clear, specific name like InsufficientFundsError. The name tells you exactly what happened. And your code can catch that one error on its own.

🧩 What Is a Custom Exception?

A custom exception is just your own error type. You define it once and then use it like any built-in error. You build it by making a new class that is based on Python’s Exception. A class is a blueprint for a kind of thing. Here that thing is an error.

The good news is you do not have to write much. Here is the entire definition of a custom exception.

class InsufficientFundsError(Exception):
pass

That is the whole thing. Let’s read it piece by piece.

  • class InsufficientFundsError names your new error. The name is yours to choose. It should say what went wrong.
  • (Exception) means your new error is based on Python’s built-in Exception. So it behaves like a real error. It can be raised and caught.
  • pass means “nothing extra inside”. You are not adding any new behaviour. You just want the name. So pass fills the body and the class is complete.

End the name with Error and you follow the same naming habit Python itself uses, like ValueError and KeyError. Anyone reading your code sees the name and knows right away that it is an exception.

Note

Always base your custom exception on Exception, not on the very top-level BaseException. Exception is the normal parent for errors you mean to catch. BaseException also covers things like the user pressing Ctrl+C to stop the program. You usually do not want to catch that by accident.

🚀 Raising Your Custom Exception

Once the class exists, you raise it just like any other error. You use the raise keyword you learned last lesson. And you can pass a message inside the brackets.

Here is the bank example again, now with the clearer error.

class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError("You tried to take out more than your balance")
return balance - amount
withdraw(100, 250)

Let’s walk through it. The withdraw function checks if the amount is bigger than the balance. Alex has 100 but asks for 250. So the check is true. That triggers raise InsufficientFundsError(...). That stops the function and reports the error with your message.

Output

Traceback (most recent call last):
File "main.py", line 10, in <module>
withdraw(100, 250)
File "main.py", line 6, in withdraw
raise InsufficientFundsError("You tried to take out more than your balance")
InsufficientFundsError: You tried to take out more than your balance

Look at the last line of that error. It says InsufficientFundsError, not ValueError. The error now names the real problem. Anyone reading that message knows at once that it was about money, not some random bad value.

🎣 Catching Your Custom Exception

The real reward comes when you catch it. Your error has its own name. So you can catch that one type on its own and handle it the right way. Other errors are not caught here. So they can be handled somewhere else.

Here we wrap the withdrawal in a try and catch only our error.

class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFundsError("Not enough balance for this withdrawal")
return balance - amount
try:
new_balance = withdraw(100, 250)
print("New balance:", new_balance)
except InsufficientFundsError as error:
print("Withdrawal blocked:", error)

Read it from the top. The try block runs the withdrawal. The check fails, so InsufficientFundsError is raised. Python then looks for an except that matches that exact type. It finds except InsufficientFundsError and runs the code there. The as error part gives you the error object. So you can print the message it carried.

Output

Withdrawal blocked: Not enough balance for this withdrawal

See how clean that is? You caught only the money problem. If the withdrawal had instead raised some other kind of error, this except would have ignored it. That is because it only listens for InsufficientFundsError. That is the whole point of a named error. You can handle each problem on its own terms.

Tip

Catching a specific type like InsufficientFundsError is much safer than catching a broad Exception. A broad catch would swallow every error, even bugs you did not expect. A specific catch deals only with the one case you planned for.

💬 Adding a Friendlier Message

Most of the time pass is all you need. But sometimes you want every one of these errors to carry the same standard message without typing it each time. You can give the class its own default message by writing a small __init__ method.

Here the error sets up its own text when no message is passed.

class InsufficientFundsError(Exception):
def __init__(self, message="Account does not have enough funds"):
super().__init__(message)
raise InsufficientFundsError()

The __init__ method runs when the error is created. It takes a message that already has a default value. Then super().__init__(message) hands that message up to the built-in Exception. That is what makes it show in the traceback. The message has a default. So you can raise the error with empty brackets and still get useful text.

Output

Traceback (most recent call last):
File "main.py", line 5, in <module>
raise InsufficientFundsError()
InsufficientFundsError: Account does not have enough funds

This is optional. If a simple named error is enough, stick with pass. Use __init__ only when a built-in default message is actually worth it.

⚠️ Common Mistakes

A few things trip people up the first time. Watch for these.

  • Forgetting to base the class on Exception. If you write class InsufficientFundsError: with nothing in the brackets, you cannot raise it. Python will complain that it is not an exception.
# ❌ Avoid: this is a plain class, not an error
class InsufficientFundsError:
pass
# ✅ Good: based on Exception, so it can be raised and caught
class InsufficientFundsError(Exception):
pass
  • Catching too broadly and losing the benefit. You make a nice specific error and then catch plain Exception. Now you throw away the one advantage you built.
# ❌ Avoid: catches everything, defeats the point
except Exception:
print("Something went wrong")
# ✅ Good: catch the exact error you care about
except InsufficientFundsError:
print("Not enough funds")
  • Naming it without Error at the end. Names like InsufficientFunds work. But InsufficientFundsError reads better and matches how Python names its own errors.

  • Putting real logic where you meant a simple marker. If all you want is a named error, pass is correct. Do not add an empty __init__ that does nothing useful.

✅ Best Practices

Keep these small habits and your custom errors stay clean and helpful.

  • End the name with Error, like InsufficientFundsError or InvalidEmailError. It matches Python’s own style and reads clearly.
  • Base every custom exception on Exception. It is the right parent for errors you plan to catch.
  • Pass a clear message when you raise it. That way the traceback explains the problem in plain words.
  • Catch the specific type, not a broad Exception. Then you only handle the case you actually planned for.
  • Use pass for a simple named error. Only add __init__ when a default message or extra detail truly helps.

🧩 What You’ve Learned

✅ A custom exception is your own error type, made by writing class MyError(Exception): pass.

✅ Basing it on Exception is what lets you raise it and catch it like any built-in error.

✅ A specific name like InsufficientFundsError says exactly what went wrong, unlike a general ValueError.

✅ You catch it on its own with except InsufficientFundsError, so other errors are left alone.

✅ Add an __init__ with super().__init__(message) only when you want a built-in default message.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    How do you define the simplest possible custom exception?

    Why: You make a class based on Exception and use pass for the body, so it works like a real error you can raise and catch.

  2. 2

    Why use a custom exception instead of a general ValueError?

    Why: A named error like InsufficientFundsError describes the real problem and can be caught separately from other errors.

  3. 3

    What does the (Exception) part do in class InsufficientFundsError(Exception):?

    Why: Basing the class on Exception is what lets it be raised and caught like any built-in error.

  4. 4

    How do you catch only your InsufficientFundsError and leave other errors alone?

    Why: except InsufficientFundsError listens for that exact type only, so unrelated errors are not caught here.

🚀 What’s Next?

You can now give your errors clear, specific names and handle each one on its own. Next we step away from errors and start working with files. We begin with how to open a file and read what is inside it.

Reading Files

Share & Connect