Safe Paws Project — Round 2/5
No arguments about persistence this time. What we said at the Calories Tracker holds here too — persistence comes when the data is mature. We're there. So the dictionary becomes a file: safe_paws.txt.
paws_dict = {}
def load_from_txt():
try:
with open("safe_paws.txt", "r", encoding="utf-8") as f:
for line in f:
row = line.strip().split(";")
paws_dict[int(row[0])] = [row[1], row[2], row[3], row[4], int(row[5]), row[6]]
except FileNotFoundError:
pass # first run — no file yet
def get_next_id():
if not paws_dict:
return 1
all_ids = []
for k in paws_dict.keys():
all_ids.append(int(k))
return max(all_ids) + 1
def add_paw():
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 = get_next_id()
with open("safe_paws.txt", "a", encoding="utf-8") as f:
f.write(f"{id};{name};{specie};{size};{health};{age};available\n")
load_from_txt()
load_from_txt()
while True:
print("\nSafe Paws Menu")
option = input("Choose your option (1-Add paw, q-Quit): ")
if option == "1":
add_paw()
print("Paw added successfully!")
elif option == "q":
break
else:
print("Invalid option!")
Quick recap of the structure. paws_dict starts empty — it gets populated from the file. load_from_txt() does the loading — it exists but doesn't run until called. get_next_id() assigns each animal the next available number. add_paw() writes to the file and reloads the dictionary immediately after. load_from_txt() is called before the menu — so the dictionary is ready when the program starts. The menu gives us two options for now: add and quit.
Now — filtering. We want an advanced search that doesn't lock the user into a single specific animal. Someone looking for a dog doesn't always care about size. Someone who didn't find what they wanted the first time might want to broaden the search.
The criteria: species, size, max age. Each one optional — entering 0 skips that filter. Status is always filtered — we only show available animals.
We start by collecting the inputs. The code is copied almost directly from add_paw() — only the prompt changes.
def adv_search():
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.")
Now the filter itself.
results = {}
for key in paws_dict:
if paws_dict[key][5] == "available":
if specie == "0" or paws_dict[key][1] == specie:
if size == "0" or paws_dict[key][2] == size:
if age == 0 or paws_dict[key][4] <= age:
results[key] = paws_dict[key]
return results
Four levels. Each one is a checkpoint. An animal passes the first — it's available. It passes the second — species matches, or the user doesn't care about species. It passes the third — size matches, or skipped. It passes the fourth — age is within range, or skipped. Only animals that clear every checkpoint make it into results.
0 disables the checkpoint entirely — everyone passes. It's not a workaround. It's a design decision. The user controls how wide or narrow the search is.
One note about age and health — these fields matter, and we include age here as an exercise. In practice, filtering by age or health status risks excluding animals that deserve a chance. Someone always picks the young, healthy one and ignores the rest. That's a conversation for another day. For now, the filter works.
Now a print function.
def print_adv_search():
results = adv_search()
for key in results:
print(f"ID: {key} - {results[key][0]} is a {results[key][4]} years old, {results[key][2]} sized {results[key][1]}. Health status: {results[key][3]}.")
Assuming the shelter has these animals registered:
{
1: ['Lassie', 'dog', 'medium', 'healthy', 4, 'available'],
2: ['Whiskers', 'cat', 'small', 'healthy', 2, 'available'],
3: ['Rex', 'dog', 'big', 'healthy', 6, 'available']
}
A search for dogs, any size, max age 5:
Searched specie (0 for all): dog
Searched size (small/medium/big, 0 for all): 0
Searched max age (0 for all): 5
Output:
ID: 1 - Lassie is a 4 years old, medium sized dog. Health status: healthy.
Rex doesn't make it — age 6 is over the limit. Whiskers doesn't make it — wrong species. Lassie passes all checkpoints.
And the updated menu. We check if the search returns anything before calling the print function — if nothing matches, we say so.
load_from_txt()
while True:
print("\nSafe Paws Menu")
option = input("Choose your option (1-Add paw, 2-Adv search, q-Quit): ")
if option == "1":
add_paw()
print("Paw added successfully!")
elif option == "2":
if len(adv_search()) > 0:
print_adv_search()
else:
print("No data match your search.")
elif option == "q":
break
else:
print("Invalid option!")
Quantitatively — a lot. We couldn't sit down and write this end to end from scratch. But the logic is simple, and broken into steps, this is exactly what we built. One brick at a time.
We have persistence. We have advanced filtering. The fort is taking shape.
Next — edit, adoption and more. More bricks a stronger fort. Same method.