Python in Detail: Custom Exceptions
Welcome to the Custom Exceptions lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Let's say we're transferring funds from one bank account to another. If the account's balance is too low, we raise a
ValueError.ValueErroris a good match: it means that the argument had the correct data type, but the specific value is not acceptable.>
account_balances = {"Amir": 300,"Betty": 500,}def transfer_funds(amount, from_account, to_account):if account_balances[from_account] < amount:raise ValueError("insufficient funds")account_balances[from_account] -= amountaccount_balances[to_account] += amount- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
transfer_funds(100, "Amir", "Betty")(account_balances["Amir"], account_balances["Betty"])Result:
(200, 600)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
transfer_funds(350, "Amir", "Betty")(account_balances["Amir"], account_balances["Betty"])Result:
ValueError: insufficient funds
If we pass a non-existent account to
transfer_funds, we get aKeyErrorwhen we look upaccount_balances[...]. This happens due to standard dict behavior.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
transfer_funds(100, "Amiir", "Betty")(account_balances["Amir"], account_balances["Betty"])Result:
KeyError: 'Amiir'
This error message is less clear, but we can improve it. We'll customize the message to tell us what's really wrong: no account with that name exists. We'll also change the exception class to
ValueError, since we're now describing the problem in terms of the application (that account doesn't exist) rather than a Python data structure (the dict doesn't have that key).>
account_balances = {"Amir": 300,"Betty": 500,}def transfer_funds(amount, from_account, to_account):if from_account not in account_balances:raise ValueError(f"Account doesn't exist: {from_account}")if to_account not in account_balances:raise ValueError(f"Account doesn't exist: {to_account}")if account_balances[from_account] < amount:raise ValueError("insufficient funds")account_balances[from_account] -= amountaccount_balances[to_account] += amounttransfer_funds(100, "Amiir", "Betty")[account_balances["Amir"], account_balances["Betty"]]Result:
ValueError: Account doesn't exist: Amiir
The code detects the mistake either way. But when debugging,
ValueError("Account doesn't exist: Amiir")is more helpful than a genericKeyError.This matters even more when we consider that non-programmers sometimes see our error messages. To a bank teller,
Account doesn't exist: Amiirwill make a lot more sense thanKeyError: 'Amiir'!If we call
transfer_fundsinside of atry:, and we end up catching aValueError, does that mean that the account didn't exist? Not necessarily! A lot of built-in Python operations raise built-in exceptions likeKeyErrorandValueError.>
from math import sqrtsqrt(-1)Result:
>
int("Amir")Result:
When we plan to catch an exception, it's usually a good idea to raise an instance of a custom exception class, rather than one of Python's built-in exception classes. For example, if we catch a
NoAccountErrorexception, we can be sure that it wasn't caused by a math operation like the ones above.We can define a custom exception class by inheriting from any exception class. In this case, we'll inherit from
Exceptionitself, which is a good default choice. The next example raises aNoAccountErrorwhen an account doesn't exist. We'll catch it in a moment.>
class NoAccountError(Exception):passaccount_balances = {"Amir": 300,"Betty": 500,}def transfer_funds(amount, from_account, to_account):if from_account not in account_balances:raise NoAccountError(f"Can't transfer from {from_account}")if to_account not in account_balances:raise NoAccountError(f"Can't transfer to {to_account}")if account_balances[from_account] < amount:raise ValueError("insufficient funds")account_balances[from_account] -= amountaccount_balances[to_account] += amountNote the new error type below!
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
transfer_funds(100, "Amiir", "Betty")(account_balances["Amir"], account_balances["Betty"])Result:
NoAccountError: Can't transfer from Amiir
Now any code that calls
transfer_fundscan catchNoAccountErrors. When that happens, we can be sure that the account doesn't exist.- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
amount = 100transferred_amount = Nonetry:transfer_funds(amount, "Amiir", "Betty")transferred_amount = amountexcept NoAccountError:transferred_amount = 0transferred_amountResult:
0
Here's a code problem:
The bank limits daily account transfers to $1,000. Trying to transfer over the limit currently raises a generic
ValueError. Define a new exception class,TransferLimitError. Then raise the exception intransfer_fundsinstead of aValueError.A hint: your custom exception doesn't need any additional functionality, so its body can simply be
pass.account_balances = {"Amir": 2150,"Betty": 300,"Cindy": 4200,}class TransferLimitError(Exception):passdef transfer_funds(amount, from_account, to_account):if from_account not in account_balances:raise KeyError(f"Can't transfer from {from_account}")if to_account not in account_balances:raise KeyError(f"Can't transfer to {to_account}")if account_balances[from_account] < amount:raise ValueError("Insufficient funds")if amount > 1000:raise TransferLimitError("Transfer limit exceeded.")account_balances[from_account] -= amountaccount_balances[to_account] += amountover_limit_transfer = lambda: transfer_funds(1200, "Amir", "Betty")assert_raises(TransferLimitError, over_limit_transfer)assert_raises(KeyError, lambda: transfer_funds(120, "Bettty", "Cindy"))assert_raises(KeyError, lambda: transfer_funds(550, "Betty", "Amiir"))transfer_funds(500, "Cindy", "Betty")assert account_balances["Cindy"] == 3700assert account_balances["Betty"] == 800assert account_balances["Amir"] == 2150- Goal:
None
- Yours:
None