Python exceptions exercises solutions cheat sheet try except raise

Python exceptions exercises — write code that doesn’t break

Python exceptions exercises are where robust programming becomes second nature. You’ve seen the theory and built four complete programs. Now it’s time to solve challenges on your own — a password validator, a unit converter and a contact book with custom exceptions.

As always: try to solve it yourself, use the hint if stuck for more than 10 minutes, and compare with the commented solution. Use pythontutor.com to step through your solution.


Python exceptions exercises — Basic Level

Exercise 1 — Robust password validator

Write a program that asks for a password and validates it with these requirements, giving specific feedback for each rule that fails. Keep asking until a valid password is entered.

Requirements:

At least 8 characters
At least one uppercase letter
At least one lowercase letter
At least one digit
At least one special character (!@#$%^&*)
=== PASSWORD VALIDATOR ===

Password: hello
  ✗ Too short (5 characters, minimum 8)
  ✗ No uppercase letters
  ✗ No digits
  ✗ No special characters (!@#$%^&*)
  Errors: 4 — try again

Password: Hello123
  ✗ No special characters (!@#$%^&*)
  Errors: 1 — try again

Password: Hello123!
  ✓ Password accepted

Strength: Strong (5/5 criteria met)

💡 Hints:

  • Create a validate_password(password) function that returns a list of error strings
  • Use any(c.isupper() for c in password) to check uppercase
  • Raise a custom WeakPasswordError(Exception) if the list is not empty, passing the error list as an attribute
  • Catch it in the while True loop, print each error, and continue

Exercise 2 — Robust unit converter

Write a program that converts between units with complete exception handling. The user enters a value, source unit and target unit. Handle all possible errors gracefully.

Supported conversions:

Length:      m ↔ km ↔ cm ↔ mm ↔ ft ↔ in
Weight:      kg ↔ g ↔ lb ↔ oz
Temperature: C ↔ F ↔ K
=== UNIT CONVERTER ===

Value: abc
  ✗ Value must be a number

Value: 100
Source unit: metres
  ✗ Unknown unit: 'metres'
  Available: m, km, cm, mm, ft, in, kg, g, lb, oz, C, F, K

Source unit: m
Target unit: ft

100 m = 328.08 ft

Convert again? (y/n): y

Value: -300
Source unit: C
Target unit: K
  ✗ Temperature -300°C is below absolute zero (-273.15°C)

Convert again? (y/n): n
Goodbye!

💡 Hints:

  • Use a dictionary of conversion factors to metres and kg as base units
  • Create InvalidUnitError and InvalidTemperatureError custom exceptions
  • Temperature needs special treatment — convert to base unit (Celsius) first, then to target
  • Wrap the entire conversion attempt in try/except and loop with while True

Python exceptions exercises — Intermediate Level

Exercise 3 — Contact book with custom exceptions

Build a contact book application with complete exception handling. Each contact has name, phone and email. The application must validate all data and use custom exceptions throughout.

=== CONTACT BOOK ===

1. Add contact
2. Search contact
3. Delete contact
4. List all contacts
5. Export to file
0. Exit

Option: 1
Name: Sergio Medina
Phone: 612345678
Email: sergio@sergiolearns.com
  ✓ Contact added: Sergio Medina

Option: 1
Name: Ana
Phone: not-a-phone
Email: invalid-email
  ✗ Phone: must be 9 digits
  ✗ Email: must contain @ and a domain

Option: 2
Search name: sergio
  Found: Sergio Medina | 612345678 | sergio@sergiolearns.com

Option: 3
Name to delete: Nobody
  ✗ Contact not found: Nobody

Option: 5
Filename: contacts.txt
  ✓ Exported 1 contact(s) to contacts.txt

💡 Hints:

  • Create these custom exceptions: ContactError, ContactNotFoundError, DuplicateContactError, InvalidPhoneError, InvalidEmailError
  • Phone validation: len(phone) == 9 and phone.isdigit()
  • Email validation: '@' in email and '.' in email.split('@')[1]
  • Store contacts in a dictionary: {name.lower(): contact_dict}
  • File export: wrap in try/except (FileNotFoundError, PermissionError, OSError)
  • Use finally to ensure the menu always displays

Commented solutions

Solution Exercise 1

SPECIALS = '!@#$%^&*'

class WeakPasswordError(Exception):
    def __init__(self, errors):
        self.errors = errors
        super().__init__(f'{len(errors)} requirement(s) not met')

def validate_password(password):
    errors = []

    if len(password) < 8:
        errors.append(f'Too short ({len(password)} characters, minimum 8)')
    if not any(c.isupper() for c in password):
        errors.append('No uppercase letters')
    if not any(c.islower() for c in password):
        errors.append('No lowercase letters')
    if not any(c.isdigit() for c in password):
        errors.append('No digits')
    if not any(c in SPECIALS for c in password):
        errors.append(f'No special characters ({SPECIALS})')

    if errors:
        raise WeakPasswordError(errors)

    return len([
        len(password) >= 8,
        any(c.isupper() for c in password),
        any(c.islower() for c in password),
        any(c.isdigit() for c in password),
        any(c in SPECIALS for c in password)
    ])

def get_password():
    print('=== PASSWORD VALIDATOR ===\n')

    while True:
        password = input('Password: ')

        try:
            score = validate_password(password)
            print(f'  ✓ Password accepted')
            strength = 'Very strong' if score == 5 else 'Strong'
            print(f'\nStrength: {strength} ({score}/5 criteria met)')
            return password

        except WeakPasswordError as err:
            for error in err.errors:
                print(f'  ✗ {error}')
            print(f'  Errors: {len(err.errors)} — try again\n')

get_password()

Solution Exercise 2

class InvalidUnitError(Exception):
    def __init__(self, unit, available):
        self.unit = unit
        self.available = available
        super().__init__(
            f"Unknown unit: '{unit}'\n  Available: {', '.join(sorted(available))}"
        )

class InvalidTemperatureError(Exception):
    def __init__(self, value, unit, minimum):
        super().__init__(
            f'Temperature {value}°{unit} is below absolute zero ({minimum}°{unit})'
        )

# Conversion factors to base units (metres for length, kg for weight)
TO_METRES = {'m': 1, 'km': 1000, 'cm': 0.01, 'mm': 0.001,
             'ft': 0.3048, 'in': 0.0254}
TO_KG = {'kg': 1, 'g': 0.001, 'lb': 0.453592, 'oz': 0.0283495}
TEMP_UNITS = {'C', 'F', 'K'}
ALL_UNITS = set(TO_METRES) | set(TO_KG) | TEMP_UNITS

def to_celsius(value, unit):
    if unit == 'C': return value
    if unit == 'F': return (value - 32) * 5/9
    if unit == 'K': return value - 273.15

def from_celsius(celsius, unit):
    if unit == 'C': return celsius
    if unit == 'F': return celsius * 9/5 + 32
    if unit == 'K': return celsius + 273.15

def convert(value, from_unit, to_unit):
    if from_unit not in ALL_UNITS:
        raise InvalidUnitError(from_unit, ALL_UNITS)
    if to_unit not in ALL_UNITS:
        raise InvalidUnitError(to_unit, ALL_UNITS)

    # Temperature
    if from_unit in TEMP_UNITS or to_unit in TEMP_UNITS:
        if from_unit not in TEMP_UNITS or to_unit not in TEMP_UNITS:
            raise InvalidUnitError(
                f'{from_unit}→{to_unit}',
                ALL_UNITS
            )
        celsius = to_celsius(value, from_unit)
        if celsius < -273.15:
            raise InvalidTemperatureError(value, from_unit, -273.15)
        return from_celsius(celsius, to_unit)

    # Length
    if from_unit in TO_METRES and to_unit in TO_METRES:
        return value * TO_METRES[from_unit] / TO_METRES[to_unit]

    # Weight
    if from_unit in TO_KG and to_unit in TO_KG:
        return value * TO_KG[from_unit] / TO_KG[to_unit]

    raise InvalidUnitError(
        f'{from_unit}→{to_unit} (different categories)',
        ALL_UNITS
    )

def run_converter():
    print('=== UNIT CONVERTER ===\n')

    while True:
        try:
            value_str = input('Value: ')
            value = float(value_str)
        except ValueError:
            print('  ✗ Value must be a number\n')
            continue

        from_unit = input('Source unit: ').strip()
        to_unit = input('Target unit: ').strip()

        try:
            result = convert(value, from_unit, to_unit)
            print(f'\n{value} {from_unit} = {round(result, 4)} {to_unit}\n')
        except InvalidTemperatureError as err:
            print(f'  ✗ {err}\n')
        except InvalidUnitError as err:
            print(f'  ✗ {err}\n')

        again = input('Convert again? (y/n): ').strip().lower()
        if again != 'y':
            print('Goodbye!')
            break
        print()

run_converter()

Solution Exercise 3

import os

# Custom exception hierarchy
class ContactError(Exception):
    pass

class ContactNotFoundError(ContactError):
    def __init__(self, name):
        super().__init__(f'Contact not found: {name}')

class DuplicateContactError(ContactError):
    def __init__(self, name):
        super().__init__(f'Contact already exists: {name}')

class InvalidPhoneError(ContactError):
    def __init__(self, phone):
        super().__init__(f'Phone must be 9 digits, got: {phone}')

class InvalidEmailError(ContactError):
    def __init__(self, email):
        super().__init__(f'Email must contain @ and a domain, got: {email}')

# Contact book
contacts = {}

def validate_phone(phone):
    phone = phone.strip()
    if not (len(phone) == 9 and phone.isdigit()):
        raise InvalidPhoneError(phone)
    return phone

def validate_email(email):
    email = email.strip().lower()
    parts = email.split('@')
    if len(parts) != 2 or '.' not in parts[1]:
        raise InvalidEmailError(email)
    return email

def add_contact(name, phone, email):
    name = name.strip().title()
    if not name:
        raise ContactError('Name cannot be empty')

    key = name.lower()
    if key in contacts:
        raise DuplicateContactError(name)

    errors = []
    clean_phone = phone
    clean_email = email

    try:
        clean_phone = validate_phone(phone)
    except InvalidPhoneError as err:
        errors.append(f'Phone: {err}')

    try:
        clean_email = validate_email(email)
    except InvalidEmailError as err:
        errors.append(f'Email: {err}')

    if errors:
        for err in errors:
            print(f'  ✗ {err}')
        return False

    contacts[key] = {
        'name': name,
        'phone': clean_phone,
        'email': clean_email
    }
    print(f'  ✓ Contact added: {name}')
    return True

def search_contact(query):
    query = query.strip().lower()
    results = 
    if not results:
        raise ContactNotFoundError(query)
    return results

def delete_contact(name):
    key = name.strip().lower()
    if key not in contacts:
        raise ContactNotFoundError(name.strip())
    deleted = contacts.pop(key)
    print(f'  ✓ Deleted: {deleted["name"]}')

def list_contacts():
    if not contacts:
        print('  No contacts yet')
        return
    print(f'  {len(contacts)} contact(s):')
    for c in sorted(contacts.values(), key=lambda x: x['name']):
        print(f'  • {c["name"]} | {c["phone"]} | {c["email"]}')

def export_contacts(filename):
    filename = filename.strip()
    if not filename.endswith('.txt'):
        filename += '.txt'
    try:
        with open(filename, 'w', encoding='utf-8') as f:
            f.write('=== CONTACT BOOK EXPORT ===\n\n')
            for c in sorted(contacts.values(), key=lambda x: x['name']):
                f.write(f'Name:  {c["name"]}\n')
                f.write(f'Phone: {c["phone"]}\n')
                f.write(f'Email: {c["email"]}\n')
                f.write('-' * 30 + '\n')
        print(f'  ✓ Exported {len(contacts)} contact(s) to {filename}')
    except PermissionError:
        raise ContactError(f'No permission to write: {filename}')
    except OSError as err:
        raise ContactError(f'Export error: {err}')

def run_contact_book():
    print('=== CONTACT BOOK ===')

    while True:
        try:
            print('\n1. Add contact')
            print('2. Search contact')
            print('3. Delete contact')
            print('4. List all contacts')
            print('5. Export to file')
            print('0. Exit')

            option = input('\nOption: ').strip()

            if option == '0':
                print('Goodbye!')
                break

            elif option == '1':
                name = input('Name: ')
                phone = input('Phone: ')
                email = input('Email: ')
                add_contact(name, phone, email)

            elif option == '2':
                query = input('Search name: ')
                try:
                    results = search_contact(query)
                    for c in results:
                        print(f'  Found: {c["name"]} | {c["phone"]} | {c["email"]}')
                except ContactNotFoundError as err:
                    print(f'  ✗ {err}')

            elif option == '3':
                name = input('Name to delete: ')
                try:
                    delete_contact(name)
                except ContactNotFoundError as err:
                    print(f'  ✗ {err}')

            elif option == '4':
                list_contacts()

            elif option == '5':
                filename = input('Filename: ')
                try:
                    export_contacts(filename)
                except ContactError as err:
                    print(f'  ✗ {err}')

            else:
                print('  ✗ Invalid option — choose between 0 and 5')

        except KeyboardInterrupt:
            print('\n\nInterrupted — goodbye!')
            break

run_contact_book()

Visualise with Python Tutor

Copy this code into pythontutor.com and step through it:

class WeakPasswordError(Exception):
    def __init__(self, errors):
        self.errors = errors
        super().__init__(f'{len(errors)} error(s)')

def validate(password):
    errors = []
    if len(password) < 8:
        errors.append('Too short')
    if not any(c.isupper() for c in password):
        errors.append('No uppercase')
    if errors:
        raise WeakPasswordError(errors)
    return True

try:
    validate('hello')
except WeakPasswordError as err:
    for e in err.errors:
        print(e)
    print(f'Total: {len(err.errors)}')
finally:
    print('Validation complete')

Step through and observe how raise WeakPasswordError(errors) creates the exception object — Python Tutor shows it as an object with an errors attribute (the list) and the parent Exception message. When it’s caught as err, err.errors is just a regular Python list you can iterate over. The finally block runs regardless — even though an exception occurred. This is the key pattern: custom exception as a data container, not just an error message.


Cheat sheet — Python exceptions

# ============================================
# CHEAT SHEET — Python Exceptions
# Sergio Learns · sergiolearns.com
# ============================================

# BASIC STRUCTURE
try:
    # code that might fail
except SpecificError:
    # handle that specific error
except (Error1, Error2):
    # handle multiple types the same way
except Exception as err:
    # any exception + access to info
    # err message:    str(err) or err.args[0]
    # err type:       type(err).__name__
except:
    # any exception without info — use sparingly
else:
    # only if NO exception in try
finally:
    # ALWAYS — with or without exception

# RAISE
raise ValueError('message')       # raise new exception
raise TypeError                   # without message
raise                             # propagate current exception

# CUSTOM EXCEPTIONS
class MyError(Exception):
    pass                          # minimal — just a new type

class DetailedError(Exception):
    def __init__(self, value, message):
        self.value = value        # store structured data
        super().__init__(message)

raise DetailedError(42, 'Something went wrong with 42')

# EXCEPTION HIERARCHY
class AppError(Exception): pass       # base
class ValidationError(AppError): pass # specific
class DatabaseError(AppError): pass   # specific

try:
    raise ValidationError('bad input')
except AppError as err:          # catches ALL AppError subclasses
    print(err)

# ASSERT
assert condition, 'message'      # AssertionError if False
# For: internal invariants, programming errors
# NOT for: user input — disabled with python -O

# RETRY LOOP PATTERN
while True:
    try:
        value = int(input('Number: '))
        break          # exit on success
    except ValueError:
        print('Try again')

# RETRY WITH LIMIT
for attempt in range(3):
    try:
        value = int(input('Number: '))
        break
    except ValueError:
        if attempt == 2:
            raise   # propagate after 3 failures
        print(f'Try again ({2-attempt} left)')

# MULTIPLE VALIDATION ERRORS PATTERN
def validate(data):
    errors = []
    if not data.get('name'):
        errors.append('Name required')
    if not data.get('email'):
        errors.append('Email required')
    if errors:
        raise ValidationError(errors)

# FILE HANDLING
try:
    with open('file.txt') as f:    # with auto-closes
        content = f.read()
except FileNotFoundError:
    print('File not found')
except PermissionError:
    print('No permission')
except OSError as err:
    print(f'OS error: {err}')

# MOST COMMON EXCEPTIONS
# ValueError         — wrong value: int('hello')
# ZeroDivisionError  — division by zero
# TypeError          — wrong type: 'a' + 1
# IndexError         — out of range: lst[99]
# KeyError           — missing key: d['x']
# FileNotFoundError  — file missing
# AttributeError     — no such attribute
# AssertionError     — assert failed

# try/except vs if/else
# if/else   → prevent known, cheap-to-check errors
# try/except → manage unpredictable errors: files, user input, external

# EAFP — Python philosophy
try:
    result = risky_operation()    # attempt first
except SpecificError:
    handle()                      # ask forgiveness after

Similar Posts

One Comment

Leave a Reply

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