Throwing and your own exceptions
Problem
The ATM says "take your money"
The account keeps its balance. If the amount to withdraw is negative or there is not enough money, the method simply does nothing:
def withdraw(self, amount): if amount > 0 and amount <= self.balance: self.balance -= amountThe ATM uses it like this:
account = Account(100)account.withdraw(500)print("Take your money.")print(f"Balance: €{account.balance}")Take your money.Balance: €100The customer asks for €500 and has €100. The method quietly refuses, the ATM does not notice and says "Take your money." The error gets lost. The method could return True or False, but the caller can easily forget to check, and False does not say why.
The idea
raise: I refuse and I say why
Instead of keeping quiet, the method throws an exception:
def withdraw(self, amount): if amount <= 0: raise ValueError("The amount must be positive") if amount > self.balance: raise ValueError("Not enough money") self.balance -= amountraise ends the method like return. The line self.balance -= amount is never reached. But instead of a value, the method sends an exception, and the caller cannot miss it:
account = Account(100)try: account.withdraw(500) print("Take your money.")except ValueError as e: print(f"Declined: {e}")print(f"Balance: €{account.balance}")Declined: Not enough moneyBalance: €100ValueError("Not enough money")creates the exception object, like any object in Python. The message goes into its constructor, andprint(e)and the f-stringf"{e}"give it back.ValueErroris the ready-made exception for "the value of the argument is wrong". You use it in exactly this kind of check.
More precisely, if you are curious
- Another common ready-made exception is
TypeError: the value is of the wrong type, such as text instead of a number. For an action the object is not in a state for (withdrawing from a closed account), Python has no separate ready-made exception. Then you make your own, as you will see shortly. raiseworks only with an object of a subclass ofBaseException. You cannot throw a string:raise "Not enough money"stops withTypeError: exceptions must derive from BaseException.
See it
The exception falls through
def withdraw(self, amount): if amount > self.balance: raise ValueError("Not enough money") self.balance -= amount
def pay_rent(account): account.withdraw(800) print("The rent is paid.")
try: pay_rent(Account(500))except ValueError as e: print(f"Declined: {e}")Step by step, in words
- The program calls pay_rent for an account with €500.
- pay_rent calls withdraw(800): a new frame on top.
- 800 > 500: the method throws an exception with raise.
- The balance does not change: the frame is thrown away without finishing its lines.
- pay_rent has no except: "The rent is paid." is not printed, and the frame is thrown away.
- The except in the program catches the exception, and the program continues from here.
Remember the call stack from the recursion lesson. At the bottom is the program: the top level code. It calls pay_rent, and that calls withdraw. The frames sit one on top of another.
When withdraw throws, the exception looks for an except by going down the stack. A frame without a matching except is thrown away right away, without finishing its lines. That is what happens to pay_rent too. Only the program has an except, and it continues from there.
If there were no except there either, the program would stop and print a stack trace: exactly these frames.
More precisely, if you are curious
Here is the same program, but nobody catches (the file rent.py):
class Account: def __init__(self, balance): self.balance = balance
def withdraw(self, amount): if amount > self.balance: raise ValueError("Not enough money") self.balance -= amount
def pay_rent(account): account.withdraw(800) print("The rent is paid.")
pay_rent(Account(500))Traceback (most recent call last): File "/projects/rent/rent.py", line 16, in <module> pay_rent(Account(500)) File "/projects/rent/rent.py", line 12, in pay_rent account.withdraw(800) File "/projects/rent/rent.py", line 7, in withdraw raise ValueError("Not enough money")ValueError: Not enough moneyIn Python the stack trace is called a traceback, and the frames go from the outside in: at the top is the program (<module>), at the bottom is withdraw, and the last line is the kind and the message. "most recent call last" means the latest call is at the bottom. So in Python, start reading from the bottom.
Try it
What reaches the screen?
The account has €50, and the order is €120.
What will the program print?
def checkout(account): print("Paying for the order") account.withdraw(120) print("The order is paid")
account = Account(50)try: checkout(account) print("Sending confirmation")except ValueError as e: print(f"Declined: {e}")print(f"Balance: €{account.balance}")"Paying for the order" comes before the withdrawal. Then withdraw throws, and the frame of checkout is thrown away: "The order is paid" never shows up. In the program, "Sending confirmation" is skipped too, because it is in try after the call. except prints the refusal, and the balance is untouched.
Try it
Find the bug
The rent is €800, and the account has €500. The program says "The rent is paid." Which line is the bug on?
Pick the line with the bug.
except catches the exception, and pass means "nothing": Python does not allow an empty block, so you write pass. The error disappears, just like with the ATM in the first card. You catch where you can do something sensible. Here pay_rent cannot find the money, so it should not catch. Remove the try/except: the exception will reach the program, and that will tell the person.More precisely, if you are curious
- Sometimes a function catches, does something, such as writing to a log, and throws the same exception on. In Python that is
raisewith nothing after it, inexcept. The traceback stays the original one.
def pay_rent(account): try: account.withdraw(800) except ValueError: print("Log: the rent did not go through") raise
try: pay_rent(Account(500))except ValueError as e: print(f"Declined: {e}")Log: the rent did not go throughDeclined: Not enough money- You can also throw a different exception that says better what happened, and keep the old one as the cause. In Python:
raise RuntimeError("The rent is not paid") from e. The old one is ine.__cause__, and the traceback shows both, with the line "The above exception was the direct cause of the following exception".
def pay_rent(account): try: account.withdraw(800) except ValueError as e: raise RuntimeError("The rent is not paid") from e
try: pay_rent(Account(500))except RuntimeError as e: print(f"{e}, because: {e.__cause__}")The rent is not paid, because: Not enough moneyThe idea
Your own exception
"Not enough money" is text. If the ATM wants to suggest "Deposit €30 more", it has to know how much is missing. Pulling the number out of the text is fragile. Better make your own exception class with a field:
class InsufficientFundsError(Exception): def __init__(self, shortfall): super().__init__(f"Short by €{shortfall}") self.shortfall = shortfallYour own exception is an ordinary class that inherits from Exception. The constructor passes the message to the parent with super().__init__(...). The method throws it when there is not enough money:
def withdraw(self, amount): if amount <= 0: raise ValueError("The amount must be positive") if amount > self.balance: raise InsufficientFundsError(amount - self.balance) self.balance -= amountAnd the caller catches the two cases separately:
account = Account(100)try: account.withdraw(130)except InsufficientFundsError as e: print(e) print(f"Deposit €{e.shortfall} more or choose a smaller amount.")except ValueError as e: print(f"Declined: {e}")Short by €30Deposit €30 more or choose a smaller amount.More precisely, if you are curious
- Exception names always end in
Error. That is how the ready-made ones are named too:ValueError,TypeError. The PEP 8 style guide asks for it for every exception that is an error. - Your own exception can also inherit from a more specific ready-made class, such as
ValueError. Thenexcept ValueErrorcatches it too. Here it is separate, so the order of the twoexceptblocks does not matter.
The idea
Your own or ready-made?
There are no checked exceptions like in Java here. Nobody checks before the program runs whether the caller catches. What a function throws is written in its documentation: a docstring with a "Raises:" section.
def withdraw(self, amount): """Withdraws the amount from the account.
Raises: ValueError: if the amount is not positive. InsufficientFundsError: if there is not enough money. """ if amount <= 0: raise ValueError("The amount must be positive") if amount > self.balance: raise InsufficientFundsError(amount - self.balance) self.balance -= amountThis is only a habit, for people and for the editor. A program that calls withdraw without try starts and runs until the exception is really thrown. The question is a different one: do you need your own exception at all?
A ready-made exception, when the meaning matches: ValueError for a wrong value, TypeError for a wrong type. Your own, when the caller has to tell it apart from the others and react in its own way, when the exception has to carry data, like shortfall, or when there is no ready-made one with this meaning, as for withdrawing from a closed account.
Try it
Fill in the blank
An exception for an invalid age. What goes in the blank so that the message reaches the parent?
class InvalidAgeError(Exception):
def __init__(self, message):
().__init__(message)super().__init__(message) calls the parent's constructor. The parent keeps the message, and str(e) gives it back. An exception with only a message can do without __init__: class InvalidAgeError(Exception): pass inherits the constructor of Exception. You need __init__ when the exception also carries its own data, like shortfall.
Exercise
Exceptions in classes
Solve these in your editor.
1. Music shop. The shop plays instruments in a demo, but not every one can be played.
- A class
MusicalInstrumentwith a name, a brand and a price, and__init__. - An abstract class
Playable(ABC)with an abstract methodplay()(@abstractmethod). This is the Python way to make an interface. GuitarandPianoinherit from bothMusicalInstrumentandPlayable, and can play.Druminherits only from the instrument.class NotPlayableError(Exception).MusicShopwith a methoddemo_instrument(self, instrument): if the instrument can play (isinstance(instrument, Playable)), it plays it, otherwise it throwsNotPlayableError.
- In the program, demo a Fender Stratocaster guitar (€1299), Pearl Export drums (€849) and a Yamaha P-45 piano (€499) from an array. Catch the exception for each instrument separately, so the demo goes on.
Fender Stratocaster, €1299.00The guitar plays a chord.Pearl Export, €849.00Pearl Export cannot be played in a demo.Yamaha P-45, €499.00The piano plays a scale.2. An account with rules. Add to Account a method deposit(self, amount) that throws ValueError for an amount of 0 or less. Withdrawing throws InsufficientFundsError from the lesson. The account starts with €100. Read the number of operations and then the operations themselves, each on its own line: "deposit 50" or "withdraw 500". After each one, print the balance or why the operation was declined.
4deposit 50withdraw 500withdraw -5withdraw 120Balance: €150Short by €350. Balance: €150The amount must be positive. Balance: €150Balance: €303. Invalid age. Person has a name and an age. The age property (@property with a setter) throws InvalidAgeError from the card before this one if the age is below 0 or above 150. The constructor also goes through self.age = age, so that nobody can create a person with an invalid age. Create Maria (19), Peter (−3) and Ivan (200).
Maria, 19 years oldPeter: Invalid age: -3Ivan: Invalid age: 200More tasks
- Table reservation. A class
Restaurantwith a number of free tables and a method for reserving one. If the tables have run out, throw your ownNoTablesError. If the number of guests is 0 or less, throw the ready-made exception for a wrong argument. - A product with stock. A class
Productwith a quantity in stock and a methodsell(self, qty). If there is not enough in stock, throwOutOfStockError, which carries how many items there are. The shop catches the exception and offers to sell the ones in stock. - Music shop with a reason. Add to
NotPlayableErrora field with the instrument that cannot play, and print its price inexcept.
Test