Python exceptions exercises — a unit converter, passwords with their own family of errors and a contact book that always saves (and the finally that closed a file that didn’t exist)
The first version of my contact book had a beautiful finally that closed the file “whatever happened”. I tried it with an empty contact book, before the file existed, and the program blew up right in the finally: it was trying to close a file that had never been opened. The block that was meant to save me was the one that broke the program. You’ll see it live in “What’s wrong here?”, because it is one of those errors you only understand once it happens to you.
You have already seen the exceptions theory and the practice with four programs. Now it’s your turn: three exercises on three levels, three fragments with a trap, the explained solutions, an exam-style quiz and the topic’s cheat sheet to keep at hand. The error you’ll chase most today is the most common one in FP2: catching Exception (or everything with except:) instead of the specific exception, and ending up not knowing what failed.
Table of Contents
How to use this article
- Read each exercise’s statement and expected output, and write your solution without looking below.
- If you get stuck, open the hint, but try for five more minutes first.
- Before the solutions, do “What’s wrong here?”: predict what each fragment prints and then check.
- Compare your code with the commented solution. If yours works differently, that’s fine: read “Why it is done this way” to see which decisions change.
As in the practice, the solutions don’t use input(): what is typed goes in the TYPED list and read_line() shows it as in the terminal. You can use input() directly.
Python exceptions exercises — Basic Level
Exercise 1 — Unit converter that handles wrong input
Write a converter with a menu of six conversions between the metric and imperial systems. The program must not break with any input and each error must come out with its own message: an option that isn’t a number, an option that doesn’t exist, a value that isn’t a number and a negative value for a distance or a weight (temperatures can be negative).
| Option | Conversion | Formula |
|---|---|---|
| 1 | km → miles | × 0.621371 |
| 2 | miles → km | × 1.60934 |
| 3 | kg → pounds | × 2.20462 |
| 4 | pounds → kg | ÷ 2.20462 |
| 5 | Celsius → Fahrenheit | × 9/5 + 32 |
| 6 | Fahrenheit → Celsius | (x − 32) × 5/9 |
Expected output (with what a person who makes lots of mistakes would type):
=== UNIT CONVERTER === 1. km -> miles 2. miles -> km 3. kg -> pounds 4. pounds -> kg 5. Celsius -> Fahrenheit 6. Fahrenheit -> Celsius 0. Exit Choose conversion: abc Error: enter a whole number from 0 to 6 Choose conversion: 7 Error: that option does not exist Choose conversion: 1 Value to convert: -5 Error: the value cannot be negative for this conversion Choose conversion: 1 Value to convert: hello Error: enter a valid number Choose conversion: 1 Value to convert: 100 100.0 km = 62.14 miles Choose conversion: 5 Value to convert: -40 -40.0 Celsius = -40.00 Fahrenheit Choose conversion: 0 See you
Use two separate try blocks, one for the option and one for the value, each with its own except ValueError and message. If you put both conversions in the same try, you won’t know which one failed. After each error, continue goes back to the start of the loop.
Python exceptions exercises — Intermediate Level
Exercise 2 — Password validator with custom exceptions
Design a password validator with its own family of exceptions. The password must have at least 8 characters, one uppercase letter, one digit and one special character from !@#$%^&*, and it can’t contain spaces. Create this hierarchy:
| Exception | Inherits from | When it is raised |
|---|---|---|
PasswordError | Exception | Never directly: it is the parent |
LengthError | PasswordError | Fewer than 8 characters |
UppercaseError | PasswordError | No uppercase letters |
DigitError | PasswordError | No digits |
SpecialCharError | PasswordError | No special characters |
SpaceError | PasswordError | Contains a space |
The function validate_password(c) returns nothing: if it finds a problem, it raises the exception for the first one it finds. The program asks for passwords until one is valid. The first three exceptions have their own message; the last two are handled together, showing their name.
Expected output:
=== PASSWORD VALIDATOR === Password: abc Length error: at least 8 characters (it has 3) Password: abcdefgh Uppercase error: must contain at least one uppercase letter Password: Abcdefgh Digit error: must contain at least one digit Password: Abcdef1h SpecialCharError: must contain at least one special character (!@#$%^&*) Password: Abc def1! SpaceError: cannot contain spaces Password: Abcdef1! Valid password
The order of the except clauses is the key: the specific children first and except PasswordError at the end, for the rest. For “only if it is valid”, use the try‘s else. any(x.isupper() for x in c) tells you whether there is any uppercase letter.
Python exceptions exercises — Final Challenge
Exercise 3 — Contact book with file reading and writing
A contact book saved in contacts.txt, one line per contact in the format name,phone. The file you are given has two broken lines:
Laura,612345678 Pablo,698765432 Juan no comma Ana, Marta,654321987
The program must:
- Load the contact book at start-up. If the file doesn’t exist, start with an empty contact book without warning (it’s not an error: it’s the first time). If a line is wrong, warn with its number and carry on.
- Offer a menu: show all, add, search, save and exit.
- Validate the phone with a custom
PhoneErrorexception: digits only and exactly 9 of them (Spanish numbers). If it isn’t valid, ask again. - Always save on exit, even if the user interrupts the program with Ctrl+C (which raises
KeyboardInterrupt).
Expected output:
=== CONTACT BOOK === Line 3 ignored: wrong format (missing comma) Line 4 ignored: empty name or phone Contact book loaded: 3 contacts 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 1 Laura: 612345678 Pablo: 698765432 Marta: 654321987 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 2 Name: Ana Phone: 123abc Error: the phone can only contain digits Phone: 6121 Error: the phone must have 9 digits (it has 4) Phone: 612111222 Contact added: Ana, 612111222 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 3 Name to search: Juan 'Juan' is not in the contact book 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 9 Invalid option 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 0 Contact book saved: 4 contacts See you
The try that protects each line goes inside the loading for. To “always save”, wrap the whole menu in a try with except KeyboardInterrupt and a finally that saves. To search, try the EAFP style: contacts[name] inside a try with except KeyError.
What’s wrong here?
Three fragments with a trap. Read each one, predict what it prints and then check.
Fragment 1: the except that never gets its turn
try:
validate('abc')
except PasswordError:
print('Invalid password')
except LengthError as err:
print('Length error:', err)
validate('abc') raises LengthError (it is shorter than 8). Which message comes out? Here is the full program:
class PasswordError(Exception):
pass
class LengthError(PasswordError):
pass
def validate(c):
if len(c) < 8:
raise LengthError(f'at least 8 characters (it has {len(c)})')
try:
validate('abc')
except PasswordError:
print('Invalid password')
except LengthError as err:
print('Length error:', err)
Output:
Invalid password
The generic message comes out, not the length one. LengthError is a child of PasswordError, so it is also a PasswordError. Python tries the except clauses top to bottom and enters the first match: the parent’s. The child’s never runs, and Python gives no warning.
Ordering the except clauses “from general to specific”, like the index of a book. It’s the other way round: children first, then the parent.
The fix is to swap the order:
class PasswordError(Exception):
pass
class LengthError(PasswordError):
pass
def validate(c):
if len(c) < 8:
raise LengthError(f'at least 8 characters (it has {len(c)})')
try:
validate('abc')
except LengthError as err: # the child, first
print('Length error:', err)
except PasswordError: # the parent picks up the rest
print('Invalid password')
Output:
Length error: at least 8 characters (it has 3)
If you’re not sure whether one exception is a child of another, check with issubclass(LengthError, PasswordError), which gives True.
Fragment 2: the finally that breaks the program
This was the one from my contact book. The function counts the contacts in a file and, “to be safe”, closes it in the finally:
def count_contacts(name):
try:
file = open(name, encoding='utf-8')
return len(file.readlines())
except FileNotFoundError:
print('Not found:', name)
finally:
file.close()
print(count_contacts('old_contacts.txt'))
What happens if old_contacts.txt doesn’t exist?
Not found: old_contacts.txt
Traceback (most recent call last):
File "close_file.py", line 11, in <module>
print(count_contacts('old_contacts.txt'))
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "close_file.py", line 8, in count_contacts
file.close()
^^^^
UnboundLocalError: cannot access local variable 'file' where it is not associated with a value
First “Not found: …” is printed: the except FileNotFoundError worked. But then the finally runs, and file doesn’t exist: open() failed before the assignment file = … could happen. Result: UnboundLocalError inside the finally, a brand new exception that nobody catches.
Using in finally a variable that is created inside the try. If the line that creates it is the one that fails, the variable doesn’t exist.
There are two fixes: file = None before the try and if file is not None: in the finally (as in the grade reader of the practice), or, better, with, which only closes what it managed to open:
def count_contacts(name):
try:
with open(name, encoding='utf-8') as file: # with closes on its own, and only if it opened
return len(file.readlines())
except FileNotFoundError:
print('Not found:', name)
return 0
print(count_contacts('old_contacts.txt'))
Output:
Not found: old_contacts.txt 0
With files, use with open(...) as f: almost always. It is a try/finally already written, and written well.
Fragment 3: the raise that doesn’t know what to raise
Someone wants validate_phone to “raise an error” if the phone has letters, and writes this:
def validate_phone(phone):
if not phone.isdigit():
raise # 'raise the error'... but which one?
validate_phone('123abc')
Which exception comes out?
Traceback (most recent call last):
File "bare_raise.py", line 6, in <module>
validate_phone('123abc')
~~~~~~~~~~~~~~^^^^^^^^^^
File "bare_raise.py", line 3, in validate_phone
raise # 'raise the error'... but which one?
^^^^^
RuntimeError: No active exception to reraise
RuntimeError: No active exception to reraise. A raise with nothing after it means “re-raise the exception I am handling”, which only makes sense inside an except. Here there is no active exception, so Python raises a RuntimeError that says nothing about the phone.
Writing a bare raise as if it meant “raise an error”. You have to say which one: raise PhoneError('message').
The fix, with the custom exception and a message that says which value failed:
class PhoneError(Exception):
pass
def validate_phone(phone):
if not phone.isdigit():
raise PhoneError(f'{phone!r}: can only contain digits')
try:
validate_phone('123abc')
except PhoneError as err:
print('Error:', err)
Output:
Error: '123abc': can only contain digits
{phone!r} inside an f-string prints the value with its quotes ('123abc'), which reveals spaces or odd characters that would go unnoticed without them.
Commented solutions
Solution Exercise 1
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['abc', '7', '1', '-5', '1', 'hello', '1', '100', '5', '-40', '0'])
def read_line(prompt):
text = next(TYPED) # the next 'typed' value
print(prompt + text) # we show it as it would look in the terminal
return text
CONVERSIONS = { # option: (source, target, formula)
1: ('km', 'miles', lambda x: x * 0.621371),
2: ('miles', 'km', lambda x: x * 1.60934),
3: ('kg', 'pounds', lambda x: x * 2.20462),
4: ('pounds', 'kg', lambda x: x / 2.20462),
5: ('Celsius', 'Fahrenheit', lambda x: x * 9 / 5 + 32),
6: ('Fahrenheit', 'Celsius', lambda x: (x - 32) * 5 / 9),
}
NO_NEGATIVES = {1, 2, 3, 4} # distances and weights; temperatures can be negative
print('=== UNIT CONVERTER ===')
for k, (source, target, _) in CONVERSIONS.items():
print(f'{k}. {source} -> {target}')
print('0. Exit')
while True:
try: # try 1: only the option
option = int(read_line('Choose conversion: '))
except ValueError:
print('Error: enter a whole number from 0 to 6')
continue
if option == 0:
print('See you')
break
if option not in CONVERSIONS: # an integer, but not on the menu
print('Error: that option does not exist')
continue
try: # try 2: only the value
value = float(read_line('Value to convert: '))
except ValueError:
print('Error: enter a valid number')
continue
if option in NO_NEGATIVES and value < 0:
print('Error: the value cannot be negative for this conversion')
continue
source, target, formula = CONVERSIONS[option]
print(f'{value} {source} = {formula(value):.2f} {target}')
Output:
=== UNIT CONVERTER === 1. km -> miles 2. miles -> km 3. kg -> pounds 4. pounds -> kg 5. Celsius -> Fahrenheit 6. Fahrenheit -> Celsius 0. Exit Choose conversion: abc Error: enter a whole number from 0 to 6 Choose conversion: 7 Error: that option does not exist Choose conversion: 1 Value to convert: -5 Error: the value cannot be negative for this conversion Choose conversion: 1 Value to convert: hello Error: enter a valid number Choose conversion: 1 Value to convert: 100 100.0 km = 62.14 miles Choose conversion: 5 Value to convert: -40 -40.0 Celsius = -40.00 Fahrenheit Choose conversion: 0 See you
Why it is done this way:
- A dictionary of conversions: each option stores its source, its target and its formula (a
lambda). The menu is printed by looping over the dictionary, and adding a conversion means adding one line. With sixifstatements you would have six almost identical copies. - Two small
tryblocks: the first protects only the option’sint(...); the second, only the value’sfloat(...). If they fail, each gives its message andcontinuestarts again. With a singletryaround both lines, theexcept ValueErrorwouldn’t know whether the option or the value failed. option not in CONVERSIONSwith anif:7is a perfectly good integer, soint()doesn’t fail; it just isn’t on the menu. No exception is needed for something checked with such a cheapif.NO_NEGATIVES: a set with the options where a negative makes no sense. Look at −40: −40 × 9/5 + 32 = −72 + 32 = −40, and the program accepts it because option 5 isn’t in the set.
Check of the good conversion: 100 × 0.621371 = 62.1371, which :.2f rounds to 62.14.
Solution Exercise 2
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['abc', 'abcdefgh', 'Abcdefgh', 'Abcdef1h', 'Abc def1!', 'Abcdef1!'])
def read_line(prompt):
text = next(TYPED) # the next 'typed' value
print(prompt + text) # we show it as it would look in the terminal
return text
class PasswordError(Exception): # the base of the family
pass
class LengthError(PasswordError):
pass
class UppercaseError(PasswordError):
pass
class DigitError(PasswordError):
pass
class SpecialCharError(PasswordError):
pass
class SpaceError(PasswordError):
pass
SPECIALS = '!@#$%^&*'
def validate_password(c):
"""Returns nothing if valid; otherwise raises the exception of the FIRST failure."""
if len(c) < 8:
raise LengthError(f'at least 8 characters (it has {len(c)})')
if not any(x.isupper() for x in c):
raise UppercaseError('must contain at least one uppercase letter')
if not any(x.isdigit() for x in c):
raise DigitError('must contain at least one digit')
if not any(x in SPECIALS for x in c):
raise SpecialCharError(f'must contain at least one special character ({SPECIALS})')
if ' ' in c:
raise SpaceError('cannot contain spaces')
print('=== PASSWORD VALIDATOR ===')
while True:
c = read_line('Password: ')
try:
validate_password(c)
except LengthError as err:
print(' Length error:', err)
except UppercaseError as err:
print(' Uppercase error:', err)
except DigitError as err:
print(' Digit error:', err)
except PasswordError as err: # the two that are left, with their name
print(f' {type(err).__name__}: {err}')
else: # only if validate raised nothing
print(' Valid password')
break
Output:
=== PASSWORD VALIDATOR === Password: abc Length error: at least 8 characters (it has 3) Password: abcdefgh Uppercase error: must contain at least one uppercase letter Password: Abcdefgh Digit error: must contain at least one digit Password: Abcdef1h SpecialCharError: must contain at least one special character (!@#$%^&*) Password: Abc def1! SpaceError: cannot contain spaces Password: Abcdef1! Valid password
Why it is done this way:
- The exception family: six classes with no body apart from the comment. They need nothing else: their name already says what failed, and the message goes in when they are created.
validate_passwordneither prints nor returns anything: it only raises. So you can use it in a web form, in a test or in this loop without touching it.- The order of the checks decides which error you see first. With
Abc def1!everything holds except the space, soSpaceErroris raised. WithAbcdef1hthe special character is missing, which is checked before the space. - Children before the parent:
except PasswordErrorgoes last and picks upSpecialCharErrorandSpaceError.type(err).__name__says which one it was. If you put it first, fragment 1 would happen: the three custom messages would never show. elsefor success: theprint(' Valid password')and thebreakonly run ifvalidate_passwordfinished without raising anything. Putting them inside thetry, right after the call, would work the same here, butelsemakes it clear that part is not protected.
Trace of the six passwords:
| Password | Length | Uppercase? | Digit? | Special? | Space? | Result |
|---|---|---|---|---|---|---|
abc | 3 | (not checked) | LengthError | |||
abcdefgh | 8 | No | UppercaseError | |||
Abcdefgh | 8 | Yes | No | DigitError | ||
Abcdef1h | 8 | Yes | Yes | No | SpecialCharError | |
Abc def1! | 9 | Yes | Yes | Yes | Yes | SpaceError |
Abcdef1! | 8 | Yes | Yes | Yes | No | Valid |
Solution Exercise 3
# What the person would type (on your computer, replace read_line() with input()):
TYPED = iter(['1', '2', 'Ana', '123abc', '6121', '612111222', '3', 'Juan', '9', '0'])
def read_line(prompt):
text = next(TYPED) # the next 'typed' value
print(prompt + text) # we show it as it would look in the terminal
return text
class PhoneError(Exception):
pass
def load_contacts(filename):
contacts = {}
try:
with open(filename, encoding='utf-8') as file: # with closes on its own
for num, line in enumerate(file, start=1):
line = line.strip()
if not line:
continue
try: # a bad line does not ruin the load
parts = line.split(',')
if len(parts) != 2:
raise ValueError('wrong format (missing comma)')
name, phone = parts[0].strip(), parts[1].strip()
if not name or not phone:
raise ValueError('empty name or phone')
contacts[name] = phone
except ValueError as err:
print(f' Line {num} ignored: {err}')
except FileNotFoundError:
pass # first time: empty contact book, not an error
return contacts
def save_contacts(contacts, filename):
try:
with open(filename, 'w', encoding='utf-8') as file:
for name, phone in contacts.items():
file.write(f'{name},{phone}\n')
print(f'Contact book saved: {len(contacts)} contacts')
except PermissionError:
print('Error: no permission to save the contact book')
def validate_phone(phone):
if not phone.isdigit():
raise PhoneError('the phone can only contain digits')
if len(phone) != 9:
raise PhoneError(f'the phone must have 9 digits (it has {len(phone)})')
FILENAME = 'contacts.txt'
print('=== CONTACT BOOK ===')
contacts = load_contacts(FILENAME)
print(f'Contact book loaded: {len(contacts)} contacts')
try:
while True:
print('1. Show all | 2. Add | 3. Search | 0. Save and exit')
try:
option = int(read_line('Option: '))
except ValueError:
print(' Error: enter a number')
continue
if option == 0:
break
elif option == 1:
for name, phone in contacts.items():
print(f' {name}: {phone}')
elif option == 2:
name = read_line('Name: ').strip()
while True: # keeps asking until the phone is valid
try:
phone = read_line('Phone: ').strip()
validate_phone(phone)
break
except PhoneError as err:
print(' Error:', err)
contacts[name] = phone
print(f' Contact added: {name}, {phone}')
elif option == 3:
wanted = read_line('Name to search: ').strip()
try:
print(f' {wanted}: {contacts[wanted]}')
except KeyError: # EAFP: try it and catch
print(f' {wanted!r} is not in the contact book')
else:
print(' Invalid option')
except KeyboardInterrupt: # Ctrl+C in the terminal
print('\nInterrupted')
finally: # ALWAYS saved, even after Ctrl+C
save_contacts(contacts, FILENAME)
print('See you')
Output:
=== CONTACT BOOK === Line 3 ignored: wrong format (missing comma) Line 4 ignored: empty name or phone Contact book loaded: 3 contacts 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 1 Laura: 612345678 Pablo: 698765432 Marta: 654321987 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 2 Name: Ana Phone: 123abc Error: the phone can only contain digits Phone: 6121 Error: the phone must have 9 digits (it has 4) Phone: 612111222 Contact added: Ana, 612111222 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 3 Name to search: Juan 'Juan' is not in the contact book 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 9 Invalid option 1. Show all | 2. Add | 3. Search | 0. Save and exit Option: 0 Contact book saved: 4 contacts See you
Why it is done this way:
load_contactswith two levels: thewithand theexcept FileNotFoundErrorcover the whole file; thetryin thefor, each line. Line 3 has no comma (split(',')gives a single piece) and line 4 has an empty phone: both raise ourValueError, the line number is reported and the loading goes on.except FileNotFoundError: pass: the only place in the topic where an emptyexceptmakes sense. The file not existing the first time is not an error, it is normal. It is a specific exception we catch on purpose, not a “just in case”.PhoneErrorand the retry loop:validate_phoneraises; the menu’swhile Truecatches and asks again.123abcfails because of the letters and6121because of the length.- Searching with EAFP:
contacts[wanted]inside atrywithexcept KeyError.if wanted in contacts:would work too, but this way you practise the “try and catch” style. try/except KeyboardInterrupt/finallyaround the menu: if the user presses Ctrl+C, theexceptreports it and thefinallysaves. If they leave with option 0, thebreakexits thewhileand thefinallysaves just the same. We couldn’t show Ctrl+C with real output (nobody presses keys here), but the path is the same.
Check: three contacts loaded plus Ana, who is added, make the four saved on exit.
Exam-style quiz
Now test yourself as in the lab. FP2 quizzes are like this: about 10 questions in 10-15 minutes, single answer, multiple answer (wrong ticks subtract), true or false, gap filling, matching and “what does this code print?”. Here are 14 original questions on exceptions. Answer without running anything, press Check and read each explanation. These questions are also the bank of the FP2 theory mock exam, which will mix them with those of the other topics.
Match each line with the exception it raises when it runs:
int("twelve") | |
[1, 2, 3][5] | |
{"a": 1}["b"] | |
10 / 0 | |
"3" + 4 | |
None.upper() |
Definitions:
- NameError
- IndexError
- ZeroDivisionError
- TypeError
- ValueError
- AttributeError
- KeyError
Solution
int("twelve")→ ValueError[1, 2, 3][5]→ IndexError{"a": 1}["b"]→ KeyError10 / 0→ ZeroDivisionError"3" + 4→ TypeErrorNone.upper()→ AttributeError
int() gets a text that isn’t a number (wrong value, even though the type str is fine). The list has 3 items and you ask for position 5 (index). The dictionary has no key "b". Dividing by zero is ZeroDivisionError. Adding a str and an int mixes incompatible types. And None has no attribute upper.
NameError is left over: you’d get it by using a variable or function that doesn’t exist.
What does this program print? Write the exact output.
def divide(a, b):
try:
r = a / b
except ZeroDivisionError:
print("D", end="")
return None
else:
print("E", end="")
return r
finally:
print("F", end="")
divide(6, 0)
divide(6, 3)
print()Solution
Answer: DFEF
First call: 6 / 0 fails, so it enters the except (D) and, even with a return there, the finally runs before leaving (F). Second call: no error, so the else runs (E) and the finally again (F). Total: DFEF.
The classic trap is thinking the return skips the finally: it doesn’t, finally runs always.
What does this code do?
try:
x = int("4,5")
except Exception:
print("general")
except ValueError:
print("value")- It prints
general - It prints
value - It prints
generaland thenvalue - It prints
4.5
Solution
Answer: It prints general
Python goes through the except clauses in order and enters the first one whose type matches. Since ValueError is a subclass of Exception, the first block takes it and the second never runs. That’s why except clauses go from the most specific to the most general.
True or false?
- The
elseblock of atryonly runs if no exception was raised inside thetry. - The
finallyblock runs even if noexceptcatches the exception. - One
exceptcan catch several types if you write them in a tuple:except (ValueError, TypeError):. - A bare
except:should go first so that no error slips through. - A
tryblock can contain severalraisestatements.
Solution
- T — The
elseblock of atryonly runs if no exception was raised inside thetry. - T — The
finallyblock runs even if noexceptcatches the exception. - T — One
exceptcan catch several types if you write them in a tuple:except (ValueError, TypeError):. - F — A bare
except:should go first so that no error slips through. - T — A
tryblock can contain severalraisestatements.
A bare except: catches everything (even KeyboardInterrupt), so if you use it, it goes last; in front, it would make the other except clauses dead code. You can have as many raise statements in a try as you like (for example, one per validation).
Complete the idea of a program that asks for a number until the user types it correctly:
The conversion int(input()) goes inside the ____ block. The conversion error is caught with except ____ and the user is warned. If there was no error, the ____ block does break to leave the loop. A ____ block would run on every iteration, error or not.
Words:
- TypeError
- ValueError
- raise
- finally
- else
- except
- try
Solution
The conversion int(input()) goes inside the try block. The conversion error is caught with except ValueError and the user is warned. If there was no error, the else block does break to leave the loop. A finally block would run on every iteration, error or not.
It’s the classic robust-input pattern: while True: + try with the conversion + except ValueError + else: break. int("seven") raises ValueError (the type str is right, the value isn’t).
Which of these headers correctly store the exception in the variable e in Python 3?
except ValueError as e:except (ValueError, TypeError) as e:except e as ValueError:except ValueError, e:except as e:
Solution
Answer: except ValueError as e: · except (ValueError, TypeError) as e:
The form is always except Type as name:; the type can be a tuple of types. In the exam the trap is the order (except err as TypeError): that would try to use err as the type.
What does this program print?
def check(age):
if age < 0:
raise ValueError("negative age")
return age
try:
check(-3)
print("A")
except ValueError as e:
print(e)Solution
Answer: negative age
check(-3) raises the ValueError, so print("A") never runs: the program jumps to the except. print(e) shows the message the exception was created with: negative age.
With x = -1, what happens when assert x > 0, "x must be positive" runs?
AssertionErroris raised with the messagex must be positivex must be positiveis printed and the program carries on- Nothing:
assertonly checks inside tests ValueErroris raised
Solution
Answer: AssertionError is raised with the message x must be positive
An assertion says «this must be true here». If the condition is false, it raises AssertionError with the text after the comma. It’s for catching programmer mistakes, not for validating what the user types.
What does this program print?
def level3():
return int("x")
def level2():
try:
level3()
finally:
print("2", end="")
def level1():
try:
level2()
except ValueError:
print("1", end="")
level1()
print("!")Solution
Answer: 21!
level3 raises ValueError and doesn’t catch it: it goes up to level2. level2 has no except either, but its finally runs before letting the exception through (2). It goes up to level1, which does catch it (1). Then the program carries on normally (!).
What do you see when this runs?
try:
try:
[].pop()
except IndexError:
print("inner")
raise
except IndexError:
print("outer")innerand thenouter- Only
inner - Only
outer innerand then the program stops with a traceback
Solution
Answer: inner and then outer
[].pop() on an empty list raises IndexError. A raise with no arguments inside an except raises the exception being handled again: useful to report (or log) something and let the caller deal with it.
Which of these statements raise an exception correctly in Python?
raise ValueErrorraise ValueError("mark out of range")raise KeyError("id")raise "mark out of range"throw ValueError()
Solution
Answer: raise ValueError · raise ValueError("mark out of range") · raise KeyError("id")
You can raise the class (raise ValueError, Python creates the object) or an object with its message. What you can’t do is raise a bare string.
What does this program print?
prices = {"coffee": 1.2}
try:
print(prices["tea"])
except KeyError as e:
print("Missing", e)Solution
Answer: Missing ‘tea’
The key "tea" doesn’t exist, so KeyError is raised before print shows anything. The message of a KeyError is the key itself in its repr form, 'tea' (with quotes).
You’re creating your own exception InvalidMarkError. Which class should it inherit from?
ExceptionBaseExceptionobjectError
Solution
Answer: Exception
Custom exceptions inherit from Exception (or from another exception of yours that does). That way an except Exception catches them, but they don’t mix with the ones that stop the program. You’ll see it in depth in Inheritance.
What number does this program print?
total = 0
for item in ["4", "x", "6", ""]:
try:
total += int(item)
except ValueError:
total += 100
print(total)Solution
Answer: 210
"4" and "6" convert fine (4 + 6 = 10). "x" and the empty string "" raise ValueError and add 100 each. Total: 10 + 100 + 100 = 210. The empty string is the tricky one: it isn’t 0, it fails.
Watch it step by step in Python Tutor
In Python Tutor (choose Python 3) you can watch fragment 1, already fixed: which except is chosen for each password.
class PasswordError(Exception):
pass
class LengthError(PasswordError):
pass
def validate(c):
if len(c) < 8:
raise LengthError('at least 8 characters')
return 'valid'
for c in ['abc', 'Abcdef1!']:
try:
print(c, validate(c))
except LengthError as err:
print(c, 'isn't valid:', err)
except PasswordError:
print(c, "isn't valid")
Output:
abc isn't valid: at least 8 characters Abcdef1! valid
What to watch:
- At the start, both classes appear in the global frame;
LengthErrorkeeps a reference to its parent. - With
'abc', it entersvalidate, reaches theraiseand thevalidateframe disappears: the arrow jumps straight toexcept LengthError. except PasswordErroris not even looked at: Python has already found a match.- With
'Abcdef1!',validatereturns'valid'and theprintin thetrygets to run. - Swap the two
exceptclauses in the editor and run again: with'abc'the arrow goes to the parent’s.
Cheat sheet: exceptions in Python
The essentials in 30 seconds: whatever can fail goes in try; each expected problem, in its own except, from child to parent; else for what depends on everything having gone well; finally (or with) to tidy up. The function that detects the problem raises with raise SomeError('message'); the one that knows what to do catches.
| I want to… | Code | Result |
|---|---|---|
| Catch one type | except ValueError: | Only that type and its children |
| Several types alike | except (ValueError, KeyError): | Either of the two |
| The message | except ValueError as err: print(err) | The exception’s text |
| The type’s name | type(err).__name__ | 'ValueError' |
| Something only if no error | else: | After the except clauses |
| Something always | finally: | Even with return |
| Raise | raise ValueError(f'grade: {n}') | The exception climbs to the caller |
| Re-raise the current one | raise (inside an except) | The same one, intact |
| Translate an error | raise MyError('…') from err | Both are shown |
| My exception | class MyError(Exception): | A new class |
| A family | class Child(MyError): | except MyError catches it |
| Check an assumption | assert x > 0, 'message' | AssertionError (removed by -O) |
| Open and close safely | with open(f) as fh: | Always closed |
Templates from the exercises:
# Ask until it is valid (exercise 1 and practice)
while True:
try:
value = int(input('Value: '))
except ValueError:
print('Error: type a whole number')
continue
break
# Family and order (exercise 2)
try:
validate(c)
except LengthError as err: # children first
print('Length:', err)
except PasswordError as err: # the parent, last
print(type(err).__name__, err)
else:
print('Valid')
# File with bad lines (exercise 3)
try:
with open('contacts.txt', encoding='utf-8') as f:
for num, line in enumerate(f, start=1):
try:
name, phone = line.strip().split(',')
except ValueError:
print(f'Line {num} ignored')
except FileNotFoundError:
pass
In the third template, name, phone = … raises ValueError whether there are too many or too few pieces: another way to spot the line without a comma.
| Symptom | Likely cause |
|---|---|
| A generic message comes out and never the specific one | The parent’s except (or Exception) comes before the child’s |
The function returns 0, None or an empty list for no reason | An except: or except Exception: is hiding a typo |
UnboundLocalError in the finally | The variable is created on the try line that failed: initialise it or use with |
RuntimeError: No active exception to reraise | A bare raise outside an except |
SyntaxError in except A, B: | The tuple’s parentheses are missing |
| The program stops at the first bad line of a file | The try is outside the loop, not inside |
An assert check stops working | The program is run with python -O |
Before handing in, ask yourself:
- Does each
tryprotect only the lines that can fail? - Are the
exceptclauses ordered from specific to general, and is there a generic one I don’t need? - Am I using in
finallysomething that might not exist? - Do my
raisemessages say which value failed?
You have finished the first FP2 topic. The next one is Python classes: there you’ll see why class MyError(Exception): works and how to design your own classes with their validations.
Did this article help you? Here is how you can support SergioLearns →




What is an operating system — theory with your laptop on a study afternoon, a write() call and a CPU that never rests (and the three programs that weren’t running at the same time)