In Python, exceptions are a way to respond to errors that may occur during the execution of a program. Python has numerous built-in exceptions such as IndexError when you try to access an index that doesn't exist in a list, or TypeError when you try to perform an operation on a type that doesn't support it. Sometimes, however, it can be useful to define our own exceptions. This allows us to give meaningful error messages and to respond to specific situations that the built-in exceptions can't handle. Custom exceptions are created by defining a new class that inherits from the built-in Exception class or one of its subclasses.
Let's consider a program that works with a theoretical library system. In this system, a user should not be allowed to check out more than 5 books at a time. Here is how we might create a custom exception for this:
class TooManyBooksError(Exception): # Define a new exception class
pass
def checkout_books(user, num_books):
if num_books > 5:
# Raise an exception if the user tries to check out more than 5 books
raise TooManyBooksError('You cannot check out more than 5 books at a time.') # Raise our custom exception
# Otherwise, add the books to the user's account.
user.books += num_books
In the above example, if a user tries to check out more than 5 books at a time, our custom TooManyBooksError will be raised with a specific error message. If the exception is not caught, the program will stop execution, and the error message will be printed to the console.
We can catch this exception in the same way as built-in exceptions using a try / except block:
try:
checkout_books(user, 7) # Try to check out 7 books.
except TooManyBooksError as e: # Catch our custom exception
print(e) # Print the error message.
If the checkout_books(user, 7) line raises a TooManyBooksError, the program will catch it, and print our error message 'You cannot check out more than 5 books at a time.'. This way, we can handle the error gracefully and the program can continue running.
Just like with any other class, we can add methods to our exception class. For example, let's add a method that calculates how many books the user needs to return in order to check out the desired amount:
class TooManyBooksError(Exception):
def __init__(self, attempted, limit=5, checked_out=0):
self.attempted = attempted
self.limit = limit
self.checked_out = checked_out
self.message = f'You tried to check out {self.attempted} books, but the limit is {self.limit} books.'
super().__init__(self.message)
def books_to_return(self):
return self.checked_out + self.attempted - self.limit
We can use this method after catching the exception:
try:
checkout_books(user, 7)
except TooManyBooksError as e:
print(e) # You tried to check out 7 books, but the limit is 5 books.
print(f'You need to return {e.books_to_return()} books.') # You need to return 2 books.
So, custom exceptions can have their own attributes and methods, making them a powerful tool for managing errors in a way that is tailored to your program's specific needs.
Challenge: Age Database
In a city where everybody knows everybody, the local government is trying to implement an age-check system at city venues like bars, clubs, and theaters. They need a program that can, given a dictionary of people's ages, accept a name and return that person's age.
The input is handled automatically. You should implement the function find_age(name) that given a name would return the age of the person.
However, not all people might be in the city's database. If the program cannot find a name in the database, it should raise a custom exception called NameNotFoundError and print an appropriate error message: {name} not found in the database.
Input
Output
5
Alice - 24
Bob - 32
Charlie - 21
Daisy - 27
Edgar - 30
Edgar
Edgar - 30
5
Alice - 24
Bob - 32
Charlie - 21
Daisy - 27
Edgar - 30
George
NameNotFoundError: George not found in the database.
Note: Names and ages are case-sensitive and may contain spaces. The name entered should exactly match the name in the database.