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.nameinstead ofpaws_dict[key][0]— readable, no index memorizationanimal.status = "adopted"instead ofpaws_dict[id][5] = "adopted"— explicit intentShelter("city_shelter")— a second shelter in one line, independent filesprint(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.