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.

How to use this article

  1. Read each exercise’s statement and expected output, and write your solution without looking below.
  2. If you get stuck, open the hint, but try for five more minutes first.
  3. Before the solutions, do “What’s wrong here?”: predict what each fragment prints and then check.
  4. 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).

OptionConversionFormula
1km → miles× 0.621371
2miles → km× 1.60934
3kg → pounds× 2.20462
4pounds → kg÷ 2.20462
5Celsius → Fahrenheit× 9/5 + 32
6Fahrenheit → 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:

ExceptionInherits fromWhen it is raised
PasswordErrorExceptionNever directly: it is the parent
LengthErrorPasswordErrorFewer than 8 characters
UppercaseErrorPasswordErrorNo uppercase letters
DigitErrorPasswordErrorNo digits
SpecialCharErrorPasswordErrorNo special characters
SpaceErrorPasswordErrorContains 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 PhoneError exception: 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.

The order of the exceptsTwo columns: if except PasswordError comes first it catches LengthError and the child except never runs; the other way round, the child wins.validate('abc') raises LengthError: which except wins?Wrong: parent firstexcept PasswordError:winsexcept LengthError:never'Invalid password'Right: child firstexcept LengthError:winsexcept PasswordError:never'Length error: …'Python tries the excepts top to bottom and enters the FIRST one that matches (isinstance).
LengthError is also a PasswordError: that is why the parent except catches it when it comes first.

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 six if statements you would have six almost identical copies.
  • Two small try blocks: the first protects only the option’s int(...); the second, only the value’s float(...). If they fail, each gives its message and continue starts again. With a single try around both lines, the except ValueError wouldn’t know whether the option or the value failed.
  • option not in CONVERSIONS with an if: 7 is a perfectly good integer, so int() doesn’t fail; it just isn’t on the menu. No exception is needed for something checked with such a cheap if.
  • 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_password neither 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, so SpaceError is raised. With Abcdef1h the special character is missing, which is checked before the space.
  • Children before the parent: except PasswordError goes last and picks up SpecialCharError and SpaceError. type(err).__name__ says which one it was. If you put it first, fragment 1 would happen: the three custom messages would never show.
  • else for success: the print(' Valid password') and the break only run if validate_password finished without raising anything. Putting them inside the try, right after the call, would work the same here, but else makes it clear that part is not protected.

Trace of the six passwords:

PasswordLengthUppercase?Digit?Special?Space?Result
abc3(not checked)LengthError
abcdefgh8NoUppercaseError
Abcdefgh8YesNoDigitError
Abcdef1h8YesYesNoSpecialCharError
Abc def1!9YesYesYesYesSpaceError
Abcdef1!8YesYesYesNoValid

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_contacts with two levels: the with and the except FileNotFoundError cover the whole file; the try in the for, each line. Line 3 has no comma (split(',') gives a single piece) and line 4 has an empty phone: both raise our ValueError, the line number is reported and the loading goes on.
  • except FileNotFoundError: pass: the only place in the topic where an empty except makes 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”.
  • PhoneError and the retry loop: validate_phone raises; the menu’s while True catches and asks again. 123abc fails because of the letters and 6121 because of the length.
  • Searching with EAFP: contacts[wanted] inside a try with except KeyError. if wanted in contacts: would work too, but this way you practise the “try and catch” style.
  • try / except KeyboardInterrupt / finally around the menu: if the user presses Ctrl+C, the except reports it and the finally saves. If they leave with option 0, the break exits the while and the finally saves just the same. We couldn’t show Ctrl+C with real output (nobody presses keys here), but the path is the same.
Loading the contact bookFive lines of the file: three are stored and two (no comma, empty phone) raise ValueError, caught inside the loop.load_contacts('contacts.txt'): one try per linecontacts.txt1Laura,612345678stored2Pablo,698765432stored3Juan no commaValueError: no comma4Ana,ValueError: empty phone5Marta,654321987storedResult: 3 contacts. If the try wrapped the whole loop, line 3 would have stopped the load and you would only have 2.
The try inside the for isolates each line: the bad one is reported and skipped, the reading goes on.

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.

Question 1Match

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"] → KeyError
  • 10 / 0 → ZeroDivisionError
  • "3" + 4 → TypeError
  • None.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.

Question 2Short answer

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.

Question 3Single choice

What does this code do?

try:
    x = int("4,5")
except Exception:
    print("general")
except ValueError:
    print("value")
  1. It prints general
  2. It prints value
  3. It prints general and then value
  4. 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.

Question 4True or false

True or false?

  1. The else block of a try only runs if no exception was raised inside the try.
  2. The finally block runs even if no except catches the exception.
  3. One except can catch several types if you write them in a tuple: except (ValueError, TypeError):.
  4. A bare except: should go first so that no error slips through.
  5. A try block can contain several raise statements.
Solution
  • T — The else block of a try only runs if no exception was raised inside the try.
  • T — The finally block runs even if no except catches the exception.
  • T — One except can 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 try block can contain several raise statements.

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

Question 5Fill in the gaps

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

Question 6Multiple · wrong ticks subtract

Which of these headers correctly store the exception in the variable e in Python 3?

  1. except ValueError as e:
  2. except (ValueError, TypeError) as e:
  3. except e as ValueError:
  4. except ValueError, e:
  5. 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.

Question 7Short answer

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.

Question 8Single choice

With x = -1, what happens when assert x > 0, "x must be positive" runs?

  1. AssertionError is raised with the message x must be positive
  2. x must be positive is printed and the program carries on
  3. Nothing: assert only checks inside tests
  4. ValueError is 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.

Question 9Short answer

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 (!).

Question 10Single choice

What do you see when this runs?

try:
    try:
        [].pop()
    except IndexError:
        print("inner")
        raise
except IndexError:
    print("outer")
  1. inner and then outer
  2. Only inner
  3. Only outer
  4. inner and 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.

Question 11Multiple · wrong ticks subtract

Which of these statements raise an exception correctly in Python?

  1. raise ValueError
  2. raise ValueError("mark out of range")
  3. raise KeyError("id")
  4. raise "mark out of range"
  5. 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.

Question 12Short answer

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

Question 13Single choice

You’re creating your own exception InvalidMarkError. Which class should it inherit from?

  1. Exception
  2. BaseException
  3. object
  4. Error
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.

Question 14Short answer

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:

  1. At the start, both classes appear in the global frame; LengthError keeps a reference to its parent.
  2. With 'abc', it enters validate, reaches the raise and the validate frame disappears: the arrow jumps straight to except LengthError.
  3. except PasswordError is not even looked at: Python has already found a match.
  4. With 'Abcdef1!', validate returns 'valid' and the print in the try gets to run.
  5. Swap the two except clauses 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…CodeResult
Catch one typeexcept ValueError:Only that type and its children
Several types alikeexcept (ValueError, KeyError):Either of the two
The messageexcept ValueError as err: print(err)The exception’s text
The type’s nametype(err).__name__'ValueError'
Something only if no errorelse:After the except clauses
Something alwaysfinally:Even with return
Raiseraise ValueError(f'grade: {n}')The exception climbs to the caller
Re-raise the current oneraise (inside an except)The same one, intact
Translate an errorraise MyError('…') from errBoth are shown
My exceptionclass MyError(Exception):A new class
A familyclass Child(MyError):except MyError catches it
Check an assumptionassert x > 0, 'message'AssertionError (removed by -O)
Open and close safelywith 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.

SymptomLikely cause
A generic message comes out and never the specific oneThe parent’s except (or Exception) comes before the child’s
The function returns 0, None or an empty list for no reasonAn except: or except Exception: is hiding a typo
UnboundLocalError in the finallyThe variable is created on the try line that failed: initialise it or use with
RuntimeError: No active exception to reraiseA 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 fileThe try is outside the loop, not inside
An assert check stops workingThe program is run with python -O

Before handing in, ask yourself:

  1. Does each try protect only the lines that can fail?
  2. Are the except clauses ordered from specific to general, and is there a generic one I don’t need?
  3. Am I using in finally something that might not exist?
  4. Do my raise messages 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.

Keep goingAll Python articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *