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_dictis accessible everywhere, modifiable by anything - Functions are loosely connected — nothing enforces that
load_from_txt()andadd_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_dict→self.food_dictload_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.