Skip to content
wannacode

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 -= amount

The ATM uses it like this:

account = Account(100)
account.withdraw(500)
print("Take your money.")
print(f"Balance: €{account.balance}")
Output
Take your money.
Balance: €100

The 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 -= amount

raise 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}")
Output
Declined: Not enough money
Balance: €100
  • ValueError("Not enough money") creates the exception object, like any object in Python. The message goes into its constructor, and print(e) and the f-string f"{e}" give it back.
  • ValueError is 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.
  • raise works only with an object of a subclass of BaseException. You cannot throw a string: raise "Not enough money" stops with TypeError: 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
  1. The program calls pay_rent for an account with €500.
  2. pay_rent calls withdraw(800): a new frame on top.
  3. 800 > 500: the method throws an exception with raise.
  4. The balance does not change: the frame is thrown away without finishing its lines.
  5. pay_rent has no except: "The rent is paid." is not printed, and the frame is thrown away.
  6. 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))
Error
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 money

In 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}")
Answer

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.

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 raise with nothing after it, in except. 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}")
Output
Log: the rent did not go through
Declined: 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 in e.__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__}")
Output
The rent is not paid, because: Not enough money

The 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 = shortfall

Your 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 -= amount

And 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}")
Output
Short by €30
Deposit €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. Then except ValueError catches it too. Here it is separate, so the order of the two except blocks 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 -= amount

This 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)

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 MusicalInstrument with a name, a brand and a price, and __init__.
  • An abstract class Playable(ABC) with an abstract method play() (@abstractmethod). This is the Python way to make an interface.
  • Guitar and Piano inherit from both MusicalInstrument and Playable, and can play. Drum inherits only from the instrument.
  • class NotPlayableError(Exception).
  • MusicShop with a method demo_instrument(self, instrument): if the instrument can play (isinstance(instrument, Playable)), it plays it, otherwise it throws NotPlayableError.
  • 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.
Output
Fender Stratocaster, €1299.00
The guitar plays a chord.
Pearl Export, €849.00
Pearl Export cannot be played in a demo.
Yamaha P-45, €499.00
The 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.

Input
4
deposit 50
withdraw 500
withdraw -5
withdraw 120
Output
Balance: €150
Short by €350. Balance: €150
The amount must be positive. Balance: €150
Balance: €30

3. 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).

Output
Maria, 19 years old
Peter: Invalid age: -3
Ivan: Invalid age: 200

More tasks

  • Table reservation. A class Restaurant with a number of free tables and a method for reserving one. If the tables have run out, throw your own NoTablesError. If the number of guests is 0 or less, throw the ready-made exception for a wrong argument.
  • A product with stock. A class Product with a quantity in stock and a method sell(self, qty). If there is not enough in stock, throw OutOfStockError, 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 NotPlayableError a field with the instrument that cannot play, and print its price in except.

Test

Check yourself

1. What does raise ValueError("...") do?
2. The program calls a, a calls b, b throws an exception. Only the top level code has an except. What happens to the lines in a after the call to b?
3. Where is the best place to catch the exception?
4. Which class does your own exception inherit from in Python?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·