CodingNic

Capstone Challenges

Challenge 1 Extensions: Custom Exceptions, Decorators, and Generators

Capstone Challenges 30 min read

Challenge 1 Extensions: Custom Exceptions, Decorators, and Generators

Objectives

By the end of this chapter, you should be able to:

  • Extend an existing program with a custom exception
  • Add a decorator to log actions taken on your data
  • Use a generator to search your data lazily

💡 Why this matters: Real projects grow feature by feature, on top of code that already works. These extensions are optional, but each one is a chance to reach back into an earlier module and apply it somewhere that isn’t a standalone exercise.

Extension 1: A Custom Exception for Duplicates

Right now, add_contact will happily add two contacts with the same email. Fix that with a custom exception, the way you did back in the OOP module:

python
class DuplicateContactError(Exception):
    pass

Update add_contact to check for an existing contact with the same email first, and raise DuplicateContactError if it finds one:

python
def add_contact(self, contact):
    for existing in self.contacts:
        if existing.email == contact.email:
            raise DuplicateContactError(f"{contact.email} is already in your contact book")
    self.contacts.append(contact)

Then handle it wherever you’re calling add_contact:

python
try:
    book.add_contact(new_contact)
except DuplicateContactError as e:
    print(f"Couldn't add contact: {e}")

Extension 2: Logging with a Decorator

Add a decorator that logs every time a contact is added or removed: a realistic use of decorators, beyond the toy examples from that module.

python
from functools import wraps

def log_action(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"[LOG] {func.__name__} was called")
        return result
    return wrapper

Apply it to add_contact and remove_contact:

python
@log_action
def add_contact(self, contact):
    ...

@log_action
def remove_contact(self, name):
    ...

Bonus: extend log_action to also log which contact was affected, not just which method ran. You’ll need to look at args to do it.

Extension 3: Searching Lazily with a Generator

all_contacts() returns everything at once. Add a generator method instead, for searching without building an entire filtered list upfront:

python
def search_contacts(self, keyword):
    for contact in self.contacts:
        if keyword.lower() in contact.name.lower():
            yield contact
python
for match in book.search_contacts("er"):
    print(match)

For a contact book with a handful of entries this barely matters, but the same pattern is exactly how you’d handle a search across a much larger dataset without loading every match into memory at once.

Try It

  1. Add DuplicateContactError and confirm adding the same email twice raises it, while adding a genuinely new contact still works.
  2. Apply log_action to add_contact and remove_contact, and confirm a log line prints for each.
  3. Add search_contacts as a generator method, and loop over its results with a for loop.
  4. Pick one more idea of your own (an update_contact method, a command-line menu loop, an __eq__ method on Contact) and add it.

Recap

You’ve now taken a single working project and extended it with a custom exception, a decorator, and a generator: three tools from earlier in the course, applied to real code instead of an isolated example.

Next challenge: a command-line Inventory Manager, pulling in abstract base classes, recursion, logging, and argparse.