← Back to Blog

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.

[ login to bookmark ] // copied! 32 views · 4 min
← prev Calories Tracker Update — Round 2/2 next → Safe Paws Update — Round 2/2
// 0 comments
// No comments yet. Be the first.
// leave a comment

// Your comment will appear after approval.