← Back to Blog

Calories Tracker Update — Round 1/2

You've built the Calories Tracker. It works. It persists. It reports.

Now we rebuild it — not because it's broken, but because we can do better. And because seeing the same problem solved two different ways is one of the best ways to understand why OOP exists.

Procedural vs OOP — honestly

Before we touch any code, let's be honest about both approaches.

Procedural — what it does well:

  • Simple to read and follow — top to bottom, no abstraction layers
  • Fast to write for small programs
  • Easy to debug — you can trace every step linearly

Procedural — where it struggles:

  • Global variables — food_dict is accessible everywhere, modifiable by anything
  • Functions are loosely connected — nothing enforces that load_from_txt() and add_food_item() belong together
  • One tracker per program — want two users? You'd need two dictionaries, two sets of functions, or careful parameter passing
  • Adding features risks breaking existing logic

OOP — what it adds:

  • Data and behavior in one place — the tracker object owns its dictionary and its methods
  • Multiple instances — two users, two trackers, zero conflicts
  • Encapsulation — the tracker controls its own data
  • Extensibility — add features to the class without touching the rest

OOP — what it costs:

  • More structure upfront — you need to think about the class before writing code
  • More abstraction — harder to follow for absolute beginners
  • Overkill for very small scripts

For a program that grows, persists, and might eventually serve multiple users — OOP wins. That's the Calories Tracker.

What becomes what

Here's the mapping — procedural on the left, OOP on the right:

# PROCEDURAL                    # OOP
food_dict = {}              →   self.food_dict = {}
load_from_txt()             →   def load(self)
add_food_item()             →   def add(self)
last_days_details()         →   def last_days_details(self)
print_details()             →   def print_details(self)
last_days_sum()             →   def last_days_sum(self)
print_target()              →   def print_target(self)
general_report()            →   def general_report(self)
export_general()            →   def export_general(self)

The logic inside each function doesn't change. What changes is where it lives — and who owns the data it operates on.

The class skeleton

import string
import datetime

class CalorieTracker:

    def __init__(self):
        self.food_dict = {}     # was: food_dict = {}

    def load(self):
        try:
            with open("food_log.txt", "r", encoding="utf-8") as f:
                for line in f:
                    row = line.strip().split(";")
                    self.food_dict[row[0]] = [row[1], float(row[2]), row[3], int(row[4])]
        except FileNotFoundError:
            pass

    def add(self):
        name = ""
        while not name:
            name = input("Food name: ")

        while True:
            try:
                quantity = float(input("Quantity: "))
                if quantity > 0:
                    break
                print("Quantity must be greater than 0.")
            except ValueError:
                print("Invalid quantity. Enter a number.")

        measure = ""
        while not measure:
            measure = input("Measure: ")

        while True:
            try:
                calories = int(input("Calories: "))
                if calories > 0:
                    break
                print("Calories must be greater than 0.")
            except ValueError:
                print("Invalid calories. Enter a whole number.")

        timestamp = datetime.datetime.now().strftime("%Y-%m-%d,%H:%M:%S")
        self.food_dict[timestamp] = [name, quantity, measure, calories]

        with open("food_log.txt", "a", encoding="utf-8") as f:
            f.write(f"{timestamp};{name};{quantity};{measure};{calories}\n")
        self.load()

Three changes from the procedural version — and they're all the same change:

  • food_dictself.food_dict
  • load_from_txt()self.load()
  • No global variable — the data lives on the instance

Creating and using the tracker

Constructorul primește un parametru user — fiecare instanță își generează propriul nume de fișier.

class CalorieTracker:

    def __init__(self, user):
        self.food_dict = {}
        self.filename  = f"food_log_{user}.txt"    # food_log_bull.txt, food_log_guest.txt

Creating two independent trackers:

tracker_bull  = CalorieTracker("bull")
tracker_guest = CalorieTracker("guest")

tracker_bull.load()     # loads from food_log_bull.txt
tracker_guest.load()    # loads from food_log_guest.txt

tracker_bull.add()      # writes to food_log_bull.txt only
tracker_guest.add()     # writes to food_log_guest.txt only

Two trackers. Two independent files. Zero conflicts.

Could you do this with functions?

Yes. You could pass a dictionary around:

def create_tracker(user):
    return {"food_dict": {}, "filename": f"food_log_{user}.txt"}

def add_food_item(tracker):
    ...
    tracker["food_dict"][timestamp] = [...]
    with open(tracker["filename"], "a") as f:
        ...

It works. But:

  • The tracker is a dictionary pretending to be an object — the structure is implicit, not enforced
  • Nothing stops someone from writing tracker["filename"] = "something_else" directly — no protection
  • Every function needs to receive the tracker as an argument — the connection between data and behavior is informal
  • As the program grows, the number of functions that need to know about tracker's internal structure grows with it

OOP doesn't make this possible — it was already possible. It makes it cleaner, safer, and more explicit. The object owns its filename. The object owns its dictionary. The methods are part of the object — not functions that happen to know about it.

That's the difference. Not capability — clarity.

Two trackers. Two independent dictionaries. Zero conflicts. The procedural version would require significant restructuring to achieve the same.

Round 2 completes the class — all reports, export, full menu.

[ login to bookmark ] // copied! 30 views · 3 min
← prev Making Objects Behave Like Built-ins next → Calories Tracker Update — Round 2/2
// 0 comments
// No comments yet. Be the first.
// leave a comment

// Your comment will appear after approval.