🎓 A1Learner
🔍

Python Custom Exceptions

A custom exception is an error class that names one kind of failure in an app.

Make your own error

A custom exception is a class. It inherits from Exception, like every normal error.

class EmptyNameError(Exception):
    pass

try:
    raise EmptyNameError("Name is empty")
except EmptyNameError as err:
    print(type(err).__name__)
    print(err)

Carry data as attributes

Your error can store extra values. Code that catches it can then read those values.

class ShortfallError(Exception):
    def __init__(self, needed, have):
        super().__init__(f"Need {needed}, have {have}")
        self.needed = needed
        self.have = have

try:
    raise ShortfallError(50, 20)
except ShortfallError as err:
    print(err)
    print(err.needed)
    print(err.have)

A small family of errors

One base error can have child errors. Catching the base catches every child.

class BankError(Exception):
    pass

class LowFundsError(BankError):
    pass

class FrozenError(BankError):
    pass

try:
    raise FrozenError("Wallet is frozen")
except BankError as err:
    print(type(err).__name__)
    print(err)

Check the parent class

Every custom error should pass this check. It should be an Exception under the hood.

class BankError(Exception):
    pass

print(issubclass(BankError, Exception))

Example

class BankError(Exception):
    pass

class LowFundsError(BankError):
    def __init__(self, needed, have):
        super().__init__(f"Need {needed}, have {have}")
        self.needed = needed
        self.have = have

class FrozenError(BankError):
    pass

class Wallet:
    def __init__(self, balance, frozen=False):
        self._balance = balance
        self._frozen = frozen

    def spend(self, amount):
        if self._frozen:
            raise FrozenError("Wallet is frozen")
        if amount > self._balance:
            raise LowFundsError(amount, self._balance)
        self._balance -= amount
        print(f"Spent {amount}. Left: {self._balance}")

wallet = Wallet(30)
wallet.spend(10)
try:
    wallet.spend(50)
except LowFundsError as err:
    print(type(err).__name__)
    print(err)
    print(f"Short by {err.needed - err.have}")

frozen = Wallet(30, frozen=True)
try:
    frozen.spend(5)
except BankError as err:
    print(type(err).__name__)
    print(err)

Key points

  • Custom exception: an error class you define to name one kind of failure in your app
  • Use class MyError(Exception) to define a custom exception
  • Use attributes on the exception object to carry data, such as amounts or names
  • Use one base error for a family, so one except can catch the whole family
  • Use the suffix Error at the end of custom exception names

In this example, BankError is the base of a small family. LowFundsError carries the needed and held amounts as attributes. FrozenError needs no extra data, so its body is just pass. This Wallet is the beginner course's wallet, rebuilt. Its spend now raises an error instead of printing a refusal. The Wallet class raises FrozenError when the wallet is frozen. It raises LowFundsError when a spend asks for too much. The first spend of 10 succeeds and prints the new balance. The second spend asks for 50 and fails. The except block reads err.needed and err.have to print the shortfall. The last try catches FrozenError through the shared BankError base.

Note

Never subclass BaseException directly.

It also covers system exits like KeyboardInterrupt, so subclass Exception instead.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Python Playground

Exercise

Why do we subclass Exception and not BaseException?