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.
Table of Contents
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 Trueloop, 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
InvalidUnitErrorandInvalidTemperatureErrorcustom exceptions - Temperature needs special treatment — convert to base unit (Celsius) first, then to target
- Wrap the entire conversion attempt in
try/exceptand loop withwhile 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
finallyto 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

One Comment