← Back to Blog

Safe Paws Update — Round 2/2

The shelter has its foundation. Time to complete it — edit health, register adoption, activity report.

Edit health

    def edit_health(self):
        available = {id: a for id, a in self.animals.items() if a.status == "available"}
        adopted   = {id: a for id, a in self.animals.items() if a.status == "adopted"}

        editable = False

        while True:
            try:
                id = int(input("Paw id: "))
                if id in available:
                    print(f"Paw identified: {available[id].name}")
                    editable = True
                    break
                elif id in adopted:
                    print(f"{adopted[id].name} is adopted. You can't edit an adopted paw!")
                    break
            except ValueError:
                print("Invalid ID. Enter a number.")

        if editable:
            health = ""
            while not health or health.lower() not in ["healthy", "ill"]:
                health = input("Paw health status update (healthy/ill only): ")
            self.animals[id].health = health.lower()
            print(f"{self.animals[id].name.upper()} was successfully updated. New health status is: {self.animals[id].health.upper()}.")

            with open(self.filename, "w", encoding="utf-8") as f:
                for animal in self.animals.values():
                    f.write(f"{animal.id};{animal.name};{animal.species};{animal.size};{animal.health};{animal.age};{animal.status}\n")
            self.load()

Compare to the procedural version. Instead of paws_dict[id][3] = health, we write self.animals[id].health = health. The attribute has a name. The intent is clear.

Writing to file is also cleaner — animal.id, animal.name, animal.health instead of paws_dict[key][0], paws_dict[key][3].

Register adoption

    def paw_adopted(self):
        available = {id: a for id, a in self.animals.items() if a.status == "available"}
        adopted   = {id: a for id, a in self.animals.items() if a.status == "adopted"}

        while True:
            try:
                id = int(input("Adopted paw id: "))
                if id in available:
                    owner_name = ""
                    while not owner_name:
                        owner_name = input("New owner name: ")

                    owner_contact = ""
                    while not owner_contact:
                        owner_contact = input("New owner contact: ")

                    import datetime
                    adoption_date = datetime.date.today().strftime("%Y-%m-%d")

                    with open(self.adopted_filename, "a", encoding="utf-8") as f:
                        f.write(f"{id};{owner_name};{owner_contact};{adoption_date}\n")

                    self.animals[id].status = "adopted"
                    print(f"Great! {self.animals[id].name.upper()} has a new home.")

                    with open(self.filename, "w", encoding="utf-8") as f:
                        for animal in self.animals.values():
                            f.write(f"{animal.id};{animal.name};{animal.species};{animal.size};{animal.health};{animal.age};{animal.status}\n")
                    self.load()
                    break

                elif id in adopted:
                    print(f"{adopted[id].name.upper()} was already adopted.")
                    break

            except ValueError:
                print("Invalid ID. Enter a number.")

Again — self.animals[id].status = "adopted" instead of paws_dict[id][5] = "adopted". The object knows its own status. The intent is explicit.

Activity report

    def activity_report(self):
        def get_date(item):
            return item[1][2]

        sorted_items  = sorted(self.adopted.items(), key=get_date)
        sorted_adopted = dict(sorted_items)

        report_date = datetime.date.today().strftime("%Y-%m-%d")
        filename = f"{self.name}_activity_report_{report_date}.txt"

        with open(filename, "w", encoding="utf-8") as f:
            f.write("=" * 20 + "\n")
            f.write("Adopted paws\n")
            f.write("=" * 20 + "\n")
            for id, record in sorted_adopted.items():
                animal_name = self.animals[id].name
                f.write(f"{record[2]} - {animal_name}, adopted by {record[0]} ({record[1]})\n")
            f.write("=" * 20 + "\n")
            f.write("Available paws\n")
            f.write("=" * 20 + "\n")
            for id, animal in self.animals.items():
                if animal.status == "available":
                    f.write(f"{animal.name}, a {animal.age} years old, {animal.size} sized {animal.species}.\n")

        print(f"Report saved: {filename}")

The report section is where two classes shine most. Instead of cross-referencing paws_dict[key][0] to get the animal name from the adoption record, we write self.animals[id].name. The ID links the two — same as before — but the code reads like a sentence.

The complete menu

import datetime

shelter = Shelter("safe_paws")
shelter.load()

print(shelter)

while True:
    print("\nSafe Paws Menu")
    option = input("1-Add paw\n2-Adv search\n3-Edit paw health\n4-Register adoption\n5-Activity report\nq-Quit\nChoose your option: ")
    if option == "1":
        shelter.add_paw()
        print("Paw added successfully!")
    elif option == "2":
        if len(shelter.adv_search()) > 0:
            shelter.print_adv_search()
        else:
            print("No data match your search.")
    elif option == "3":
        shelter.edit_health()
    elif option == "4":
        shelter.paw_adopted()
    elif option == "5":
        shelter.activity_report()
    elif option == "q":
        break
    else:
        print("Invalid option!")

Clean. Every operation belongs to the shelter. The menu doesn't know how anything works — it just asks.

What OOP made better — concretely

  • animal.name instead of paws_dict[key][0] — readable, no index memorization
  • animal.status = "adopted" instead of paws_dict[id][5] = "adopted" — explicit intent
  • Shelter("city_shelter") — a second shelter in one line, independent files
  • print(shelter) — instant summary via __str__
  • len(shelter) — instant count via __len__
  • Activity report exports as safe_paws_activity_report_2025-04-07.txt — shelter name included, no overwrites between shelters

What comes next

An Adopter class. Right now an adoption record is a list — name, contact, date. Functional, but limited. An Adopter class would give each person their own object:

class Adopter:
    def __init__(self, name, contact):
        self.name     = name
        self.contact  = contact
        self.adopted  = []      # list of Animal IDs

    def add_adoption(self, animal_id):
        self.adopted.append(animal_id)

One adopter, multiple animals over time. A history. A profile. Nothing in Shelter or Animal needs to change — Adopter is a one-class addition.

A search across multiple shelters. Right now adv_search() searches within one shelter. But what if a small dog isn't available at Safe Paws — could we check all shelters at once?

city_shelter   = Shelter("city_shelter")
county_shelter = Shelter("county_shelter")

city_shelter.load()
county_shelter.load()

all_shelters = [city_shelter, county_shelter]

def search_all(shelters, specie, size, age):
    results = {}
    for shelter in shelters:
        for id, animal in shelter.animals.items():
            if animal.status == "available":
                if specie == "0" or animal.species == specie:
                    if size == "0" or animal.size == size:
                        if age == 0 or animal.age <= age:
                            results[f"{shelter.name}-{id}"] = animal
    return results

A function that iterates all shelters and aggregates results. The adopter sees one unified list — Safe Paws and City Shelter combined. Each result tagged with its shelter name so you know where to go.

A ShelterNetwork class. Take that function and wrap it in a class:

class ShelterNetwork:
    def __init__(self):
        self.shelters = []

    def add_shelter(self, shelter):
        self.shelters.append(shelter)

    def search_all(self, specie, size, age):
        results = {}
        for shelter in self.shelters:
            ...
        return results

    def total_report(self):
        for shelter in self.shelters:
            print(shelter)      # uses __str__ from Shelter

One network. Many shelters. One search, one report, one object that manages them all. You have everything you need to build this — right now, with what you know.

A natural next step. An Adopter class — name, contact, adoption history. At this scale it's not necessary. But if Safe Paws grew into a real application, every adopter would deserve their own object. That's a one-class addition that changes nothing in Shelter or Animal.

The procedural version was a shelter management script. This is a shelter management system — with room to grow.

[ login to bookmark ] // copied! 33 views · 4 min
// resources
Exercise safe_paws_oop.py
← prev Safe Paws Update — Round 1/2 next → Common OOP Mistakes
// 0 comments
// No comments yet. Be the first.
// leave a comment

// Your comment will appear after approval.