Safe Paws Update — Round 1/2
We rebuilt the Calories Tracker as a class. Now we do the same for Safe Paws — but this time we go further.
The Calories Tracker had one class. Safe Paws gets two.
Why two classes?
In the procedural version, an animal was a list:
paws_dict[1] = ["Lassie", "dog", "medium", "healthy", 4, "available"]
Index 0 is the name. Index 3 is the health. Index 5 is the status. You have to remember what each position means — and hope nobody changes the order.
An Animal object is self-documenting:
lassie.name # Lassie
lassie.health # healthy
lassie.status # available
And a Shelter object owns the animals, the files, and all the operations. One shelter, fully encapsulated.
The Animal class
class Animal:
def __init__(self, id, name, species, size, health, age):
self.id = id
self.name = name
self.species = species
self.size = size
self.health = health
self.age = age
self.status = "available"
def __str__(self):
return f"[{self.id}] {self.name} | {self.species} | {self.size} | {self.health} | age: {self.age} | {self.status}"
Status defaults to "available" — no argument needed. __str__ makes printing an animal readable immediately.
lassie = Animal(1, "Lassie", "dog", "medium", "healthy", 4)
print(lassie)
Output → [1] Lassie | dog | medium | healthy | age: 4 | available
The Shelter class
class Shelter:
def __init__(self, name):
self.name = name
self.animals = {}
self.adopted = {}
self.filename = f"{name}.txt"
self.adopted_filename = f"{name}_adopted.txt"
def __str__(self):
available = sum(1 for a in self.animals.values() if a.status == "available")
adopted = sum(1 for a in self.animals.values() if a.status == "adopted")
return f"{self.name} — {available} available, {adopted} adopted"
def __len__(self):
return len(self.animals)
Shelter("safe_paws") generates safe_paws.txt and safe_paws_adopted.txt. Two shelters, two sets of files, zero conflicts.
city_shelter = Shelter("city_shelter")
county_shelter = Shelter("county_shelter")
That's what two classes give you. Not just cleaner code — a structure that scales.
Load from file
def load(self):
try:
with open(self.filename, "r", encoding="utf-8") as f:
for line in f:
row = line.strip().split(";")
animal = Animal(int(row[0]), row[1], row[2], row[3], row[4], int(row[5]))
animal.status = row[6]
self.animals[animal.id] = animal
except FileNotFoundError:
pass
try:
with open(self.adopted_filename, "r", encoding="utf-8") as f:
for line in f:
row = line.strip().split(";")
self.adopted[int(row[0])] = [row[1], row[2], row[3]]
except FileNotFoundError:
pass
Each line in the file becomes an Animal object — not a list. self.animals is a dictionary of Animal objects, keyed by ID.
Get next ID
def get_next_id(self):
if not self.animals:
return 1
all_ids = []
for key in self.animals:
all_ids.append(key)
return max(all_ids) + 1
Simpler than before — IDs are already integers in self.animals. No conversion needed.
Add a paw
def add_paw(self):
name = ""
while not name:
name = input("Paw name: ")
specie = ""
while not specie:
specie = input("Paw specie: ")
size = ""
while not size or size.lower() not in ["small", "medium", "big"]:
size = input("Paw size (small/medium/big only): ")
size = size.lower()
health = ""
while not health or health.lower() not in ["healthy", "ill"]:
health = input("Paw health status (healthy/ill only): ")
health = health.lower()
while True:
try:
age = int(input("Paw age: "))
if age > 0:
break
print("Paw age must be greater than 0.")
except ValueError:
print("Invalid paw age. Enter a number.")
id = self.get_next_id()
animal = Animal(id, name, specie, size, health, age)
self.animals[id] = animal
with open(self.filename, "a", encoding="utf-8") as f:
f.write(f"{id};{name};{specie};{size};{health};{age};available\n")
self.load()
The animal is created as an Animal object — added to self.animals and written to file. Same logic as before. Different structure.
Advanced search
def adv_search(self):
specie = input("Searched specie (0 for all): ").strip()
size = ""
while not size or size.lower() not in ["small", "medium", "big", "0"]:
size = input("Searched size (small/medium/big, 0 for all): ")
size = size.lower()
while True:
try:
age = int(input("Searched max age (0 for all): "))
if age >= 0:
break
print("Age must be 0 or greater.")
except ValueError:
print("Invalid age. Enter a number.")
results = {}
for id, animal in self.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[id] = animal
return results
def print_adv_search(self):
results = self.adv_search()
for id, animal in results.items():
print(animal)
The filter logic is identical — but instead of paws_dict[key][1], we write animal.species. Readable. Self-documenting. No index memorization.
The menu — Round 1
shelter = Shelter("safe_paws")
shelter.load()
print(shelter)
print(f"Total animals: {len(shelter)}")
while True:
print("\nSafe Paws Menu")
option = input("1-Add paw\n2-Adv search\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 == "q":
break
else:
print("Invalid option!")
Notice what changed from the procedural version. No global paws_dict. No standalone functions. The shelter owns everything — and the menu just asks.
print(shelter) gives you a readable summary before the menu even starts:
safe_paws — 20 available, 5 adopted
len(shelter) gives you the total count. Two lines that would have required manual counting in the procedural version.
Round 2 completes the shelter — edit health, register adoption, activity report.