Chapter 4 · Objects and robust code
Exceptions
Step 1 of 22 · Concept
try runs code that might fail
By the end of this lesson, your programs survive bad input and refuse bad requests.
text = input("Age: ")
try:
age = int(text)
print("Next year:", age + 1)
except ValueError:
print("Not a number:", text)
print("Done")
When a line in the try block raises a ValueError, Python skips the rest of the block. It runs the except
block instead of a traceback.
Step 2 of 22 · Quiz
Predict the output
The input is abc. Type exactly what this program prints.
text = input("Age: ")
try:
age = int(text)
print("Next year:", age + 1)
except ValueError:
print("Not a number:", text)
print("Done")
Step 3 of 22 · Demo
See it run
Run it with the input abc and compare with your answer.
Step 4 of 22 · Concept
Catch the kind you expect
try:
each = 12 // int(text)
except ValueError:
print("Not a number")
except ZeroDivisionError:
print("Not zero")
except ValueError:catches only that kind. Any other kind still stops the program with its traceback.- Several
exceptclauses can follow onetry. Python runs the first whose kind matches, and only that one. - Never write a bare
except:. It catches every kind, your own typos too, and hides them.
Step 5 of 22 · Quiz
Which except runs?
Which block runs?
try:
print(10 / 0)
except ValueError:
print("Bad number")
except ZeroDivisionError:
print("Cannot divide")
Step 6 of 22 · Concept
as e names the exception
try:
each = 12 // int(text)
except ValueError as e:
print("Try again:", e)
as egives the exception a name,e, inside itsexceptblock.print(e)andstr(e)give its message, such asinvalid literal for int() with base 10: 'abc'.- One clause can catch several kinds, in a tuple:
except (ValueError, ZeroDivisionError) as e:.
Step 7 of 22 · Demo
Ask until it is a number
A while True loop asks again until break leaves it. A good answer reaches break. A bad one raises first: an
except clause runs, and the loop asks again.
Run it. The input is abc, then 0, then 4. Each prompt shares a line with what comes next.
Step 8 of 22 · Exercise
Your turn: safe division
The loop reads three numbers and divides 100 by each. Make it survive bad input:
- Print the result when it works.
- When the input is not a number, or is 0, print
Problem:and the exception's message.
With the input 4, zero and 0, the program must print:
100 / 25.0
100 / Problem: invalid literal for int() with base 10: 'zero'
100 / Problem: division by zero
Run it first: the second number stops the program with a ValueError. Which line raised it, and which kinds can
it raise?
Put the print line in a try block, inside the loop. An except clause for each kind, with as e, prints the
message.
except ZeroDivisionError as e:
Step 9 of 22 · Concept
raise: a function refuses
raise stops a function with an exception of your choice, and a message:
class Account:
def __init__(self, owner):
self.owner = owner
self.balance = 0
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("not enough money")
self.balance -= amount
a = Account("Ada")
a.withdraw(80)
Lesson 15's withdraw printed Refused. This one refuses louder: the caller cannot miss it. When nothing catches
it, the program stops, and your message is the traceback's last line:
Traceback (most recent call last):
File "main.py", line 12, in <module>
a.withdraw(80)
~~~~~~~~~~^^^^
File "main.py", line 8, in withdraw
raise ValueError("not enough money")
ValueError: not enough money
Step 10 of 22 · Trace
The exception leaves the frames
The <module> frame calls pay inside a try. Press Next and watch the exception leave the frames.
def withdraw(balance, amount):
if amount > balance:
raise ValueError("too much")
return balance - amount
def pay(balance, price):
left = withdraw(balance, price)
print("Paid, left:", left)
return left
try:
pay(50, 80)
except ValueError as e:
print("Refused:", e)
print("End")- The
tryhas started. Line 12 is next: the callpay(50, 80). pay's frame is on the stack. Line 7 is next: it callswithdraw.withdraw's frame is on top. 80 is more than 50, so line 3 is next.- Line 3 raised
ValueError.withdrawhas notry: its frame is gone, and the exception is at line 7 ofpay. payhas notryeither. Its frame is gone too, and the exception reached thetryin<module>.- Line 13's
except ValueErrormatches.enames the exception. Line 14 is next. - Line 14 printed
Refused: too much. Theexceptblock is over, andeis gone. - Line 15 printed
End. The program is finished.
Step 11 of 22 · Quiz
Which lines never ran?
Which lines of this program never ran?
def withdraw(balance, amount):
if amount > balance:
raise ValueError("too much")
return balance - amount
def pay(balance, price):
left = withdraw(balance, price)
print("Paid, left:", left)
return left
try:
pay(50, 80)
except ValueError as e:
print("Refused:", e)
print("End")Step 12 of 22 · Concept
Your own exception class
class InsufficientFunds(Exception):
pass
- An exception is an object, and its kind is its class. Yours is a subclass of
Exception(lesson 16). passis a statement that does nothing. It fills the class's block, which needs no methods.raise InsufficientFunds("cannot take 80 from 50")raises it. Uncaught, it ends the traceback asInsufficientFunds: cannot take 80 from 50.
ZeroDivisionError really sits under ArithmeticError, itself a kind of Exception; the diagram leaves it out.
Step 13 of 22 · Demo
Catch your own
Run it. The first call works; the second raises InsufficientFunds, which the except catches by its class.
Step 14 of 22 · Exercise
Your turn: refuse with an exception
Change two places:
- In
withdraw, raiseInsufficientFundsinstead of printingRefused, with a message such ascannot take 40 from 30. - In the loop, catch it, print
Refused:and its message, and carry on.
The program must print:
Took 20
Refused: cannot take 40 from 30
Took 10
Ada: 20
Catch your own has both halves: a function that raises with an f-string message, and an except that
names the class.
In withdraw, the if block builds the message and raises. In the loop, the two lines go into a try block, so
Took 40 is skipped when it fails.
raise InsufficientFunds(msg)
Step 15 of 22 · Concept
else runs on success, finally always
try:
n = int(text)
except ValueError:
print("Not a number")
else:
print("Got", n)
finally:
print("Checked", text)
elseruns only when thetryblock raised nothing.finallyruns every time: after thetryblock, theexceptor theelse.
Step 16 of 22 · Quiz
Predict the lines
Type exactly what this program prints.
def check(text):
try:
n = int(text)
except ValueError:
print("Not a number:", text)
else:
print("Got", n)
finally:
print("Checked", text)
check("7")
check("x")
Step 17 of 22 · Demo
See it run
Run it and compare with your answer.
Step 18 of 22 · Concept
Try first, or check first?
Two ways to look up a name that may be missing (lesson 12):
if name in ages:
print(ages[name])
else:
print("unknown")
try:
print(ages[name])
except KeyError:
print("unknown")
Both work. Check first when a miss is common and the check is easy.
Try first when failing is rare, or when a check is hard to get right. text.isdigit() (lesson 9) says no to
"-5", which int reads, so try first.
Step 19 of 22 · Concept
An error while handling an error
When the except block itself fails, Python prints both tracebacks:
try:
n = int("x")
except ValueError:
print(undefined_name)
Traceback (most recent call last):
File "main.py", line 2, in <module>
n = int("x")
ValueError: invalid literal for int() with base 10: 'x'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "main.py", line 4, in <module>
print(undefined_name)
^^^^^^^^^^^^^^
NameError: name 'undefined_name' is not defined
Read the last traceback first (lesson 5): the NameError at line 4 is a bug in the except block. The first one
was caught on purpose.
Step 20 of 22 · Check yourself
Check yourself: a safe bank
Read orders until quit. No shape this time.
- Prompt with
>. Split each order into a word and an amount. depositorwithdrawthe amount, then printBalance:and the balance.- A bad amount prints
Not a number:and the amount. A refusal printsRefused:and its message. - After
quit, print the account.
The program must print:
> Balance: 50
> Balance: 30
> Refused: cannot take 100 from 30
> Not a number: ten
> Ada: 30
The shape of Ask until it is a number: a while True loop that reads, leaves with break on quit, and keeps
everything that can fail in one try block, with one except per kind.
Step 21 of 22 · Challenge · optional
Challenge: one error from another
Optional.
When int fails, parse_amount should raise BadAmount with the message not an amount: ten, linked to the
ValueError it came from. The program still stops. Its traceback must say:
The above exception was the direct cause of the following exception:
...
BadAmount: not an amount: ten
Catch the ValueError in parse_amount with as e, and raise a BadAmount in the except block.
Raised plainly there, Python says "During handling of the above exception". Add from e after the raise: it
says this error was caused by e.
raise BadAmount(f"not an amount: {text}") from e
Step 22 of 22 · Recap
Recap
try/except ValueError:runs theexceptblock instead of stopping. Catch the kinds you expect.except (A, B) as ecatches either kind;print(e)shows the message.raise ValueError("...")refuses. The exception leaves frame after frame until atrycatches it.class InsufficientFunds(Exception): passis an exception of your own.elseruns when nothing failed,finallyevery time.
Next: lesson 18 looks inside the for loop: iterators and generators.
Review
4 questions from this lesson, in a new order each time. Answer from memory: 3 of 4 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
text = input("Age: ")
try:
age = int(text)
print("Next year:", age + 1)
except ValueError:
print("Not a number:", text)
print("Done")
while True:
text = input("People: ")
try:
each = 12 // int(text)
break
except ValueError as e:
print("Try again:", e)
except ZeroDivisionError:
print("Zero people: try again")
print("Each gets", each)
for i in range(3):
text = input("100 / ")
print(100 / int(text))
class InsufficientFunds(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
msg = f"cannot take {amount} from {balance}"
raise InsufficientFunds(msg)
return balance - amount
try:
print(withdraw(50, 20))
print(withdraw(50, 80))
except InsufficientFunds as e:
print("Refused:", e)
class InsufficientFunds(Exception):
pass
class Account:
def __init__(self, owner):
self.owner = owner
self.balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
print("Refused")
return False
else:
self.balance -= amount
return True
def __str__(self):
return f"{self.owner}: {self.balance}"
a = Account("Ada")
a.deposit(50)
for amount in [20, 40, 10]:
a.withdraw(amount)
print("Took", amount)
print(a)
def check(text):
try:
n = int(text)
except ValueError:
print("Not a number:", text)
else:
print("Got", n)
finally:
print("Checked", text)
check("7")
check("x")
class InsufficientFunds(Exception):
pass
class Account:
def __init__(self, owner):
self.owner = owner
self.balance = 0
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
msg = f"cannot take {amount} from {self.balance}"
raise InsufficientFunds(msg)
self.balance -= amount
def __str__(self):
return f"{self.owner}: {self.balance}"
a = Account("Ada")
# read orders until quit
class BadAmount(Exception):
pass
def parse_amount(text):
return int(text)
parse_amount("ten")