A 50-line Python calorie tracker against the USDA API
The minimum viable tracker. SQLite, requests, no dependencies beyond stdlib + one library.
Why bother
Sometimes the right answer to “I want a calorie tracker” is “I’ll write one.” The full-featured FOSS apps are more polished but a 50-line script tells you exactly what your data is doing — because you wrote it.
This piece walks through one. SQLite for storage. USDA FDC for food data. Nothing on the network except the FDC lookup. About an hour to write from scratch the first time.
What it does
$ caltrack search "rolled oats"
1 Oats, rolled, dry 379 kcal/100g
2 Oats, rolled, cooked 71 kcal/100g
$ caltrack log 1 80
logged: Oats, rolled, dry — 80g — 303 kcal
$ caltrack today
Today (2026-04-23):
Oats, rolled, dry 80g 303 kcal
Banana, raw 118g 105 kcal
Coffee, brewed 240g 2 kcal
TOTAL 410 kcal
Search, log, and a daily total. That’s the actual minimum viable tracker.
The script
#!/usr/bin/env python3
"""caltrack — minimum viable calorie tracker."""
import sqlite3, sys, os, requests
from datetime import date
DB = os.path.expanduser("~/.caltrack.db")
KEY = os.environ.get("USDA_API_KEY")
BASE = "https://api.nal.usda.gov/fdc/v1"
def init():
c = sqlite3.connect(DB); c.executescript("""
CREATE TABLE IF NOT EXISTS food (fdc INTEGER PRIMARY KEY, name TEXT, kcal100 REAL);
CREATE TABLE IF NOT EXISTS log (id INTEGER PRIMARY KEY, day TEXT, fdc INTEGER, grams REAL);
CREATE TABLE IF NOT EXISTS last (rank INTEGER PRIMARY KEY, fdc INTEGER);
"""); c.commit(); c.close()
def search(query):
r = requests.get(f"{BASE}/foods/search", params={
"api_key": KEY, "query": query, "pageSize": 10,
"dataType": "Foundation,SR Legacy",
}, timeout=10).json()
c = sqlite3.connect(DB); c.execute("DELETE FROM last")
for i, f in enumerate(r.get("foods", []), 1):
kcal = next((n["amount"] for n in f.get("foodNutrients", [])
if n.get("nutrient", {}).get("name") == "Energy"
and n.get("nutrient", {}).get("unitName") == "kcal"), 0)
c.execute("INSERT OR REPLACE INTO food VALUES (?, ?, ?)",
(f["fdcId"], f["description"], kcal))
c.execute("INSERT INTO last VALUES (?, ?)", (i, f["fdcId"]))
print(f"{i:>3} {f['description'][:40]:40} {kcal:>5.0f} kcal/100g")
c.commit(); c.close()
def log(rank, grams):
c = sqlite3.connect(DB)
fdc = c.execute("SELECT fdc FROM last WHERE rank=?", (rank,)).fetchone()[0]
name, kcal100 = c.execute("SELECT name,kcal100 FROM food WHERE fdc=?", (fdc,)).fetchone()
c.execute("INSERT INTO log (day, fdc, grams) VALUES (?, ?, ?)",
(date.today().isoformat(), fdc, grams))
c.commit(); c.close()
print(f"logged: {name} — {grams}g — {kcal100*grams/100:.0f} kcal")
def today():
c = sqlite3.connect(DB)
print(f"Today ({date.today().isoformat()}):")
total = 0
for name, grams, kcal100 in c.execute("""
SELECT f.name, l.grams, f.kcal100 FROM log l JOIN food f ON l.fdc=f.fdc
WHERE l.day=? ORDER BY l.id""", (date.today().isoformat(),)):
kc = kcal100 * grams / 100
total += kc
print(f" {name[:30]:30} {grams:>5.0f}g {kc:>5.0f} kcal")
print(f"TOTAL {total:>5.0f} kcal")
c.close()
if __name__ == "__main__":
init()
if not KEY: sys.exit("set USDA_API_KEY")
cmd, *args = sys.argv[1:] or [""]
if cmd == "search": search(" ".join(args))
elif cmd == "log": log(int(args[0]), float(args[1]))
elif cmd == "today": today()
else: print("usage: caltrack {search|log <rank> <grams>|today}")
That’s 50 lines and it works.
What it deliberately doesn’t do
- No protein/carbs/fat tracking. Add them as columns on
foodand another print line; ~10 more lines. - No barcode scanning. CLI tool, no camera.
- No editing yesterday’s log. Add a
delete <id>command; ~5 lines. - No backup. The whole DB is
~/.caltrack.db; copy it. - No multi-user. SQLite + per-user DB. If you want multi-user, switch to Postgres and add user IDs.
- No web UI. Add Flask in another 50 lines.
- No charting. Pipe
todayfor a week into a Python notebook.
The point isn’t that this is the right tracker for everyone. The point is that the minimum is this small.
Extending it
A few patches I keep around:
Macros
Replace the kcal extraction with:
def macros(food):
out = {"kcal": 0, "protein": 0, "carbs": 0, "fat": 0}
for n in food.get("foodNutrients", []):
nut = n.get("nutrient", {})
name, unit = nut.get("name"), nut.get("unitName")
amt = n.get("amount", 0)
if name == "Energy" and unit == "kcal": out["kcal"] = amt
elif name == "Protein": out["protein"] = amt
elif name == "Carbohydrate, by difference": out["carbs"] = amt
elif name == "Total lipid (fat)": out["fat"] = amt
return out
Barcode lookup via OFF
A second lookup command that hits Open Food Facts and inserts the result into food:
def barcode(code):
r = requests.get(f"https://world.openfoodfacts.org/api/v2/product/{code}",
headers={"User-Agent": "caltrack/1.0"}, timeout=10).json()
if r.get("status") != 1: return print("not found")
p = r["product"]; n = p.get("nutriments", {})
fdc = -int(code) % 1000000 # synthetic id, negative so it doesn't collide
name = f"{p.get('brands','')} {p.get('product_name','')}".strip()
kcal = n.get("energy-kcal_100g", 0)
c = sqlite3.connect(DB)
c.execute("INSERT OR REPLACE INTO food VALUES (?,?,?)", (fdc, name, kcal))
c.execute("INSERT INTO last VALUES (1, ?)", (fdc,))
c.commit(); c.close()
print(f"1 {name} {kcal} kcal/100g (use: caltrack log 1 <g>)")
Daily total over a range
def week():
c = sqlite3.connect(DB)
for day, total in c.execute("""
SELECT l.day, SUM(f.kcal100*l.grams/100) FROM log l JOIN food f ON l.fdc=f.fdc
WHERE l.day >= date('now','-7 days') GROUP BY l.day ORDER BY l.day"""):
print(f"{day} {total:>5.0f} kcal")
Why we like this
You wrote it. You know exactly what data goes where. There is no telemetry. There is no subscription. There are no auto-renewals. There is no “we may share with marketing partners.”
Tracker apps deserve the same treatment as journal apps: there’s a class of personal-data tool that should be no more complicated than a text file with a script in front of it. This is that.
Where to take it next
If you find yourself wanting more features, the right next step is not to add them to this script. The right next step is to use one of the FOSS apps that has them. We don’t recommend extending a 50-line script into a 5,000-line one — at that point you should be running OpenNutriTracker or Waistline.
But the 50-line version is a great way to understand what’s actually happening when you log a meal. We use it for one specific use case (annotating training meals before workouts) and the FOSS apps for general logging.
References
- USDA FDC API: fdc.nal.usda.gov/api-guide
- Open Food Facts API: world.openfoodfacts.org/data
- USDA FDC getting started
- Open Food Facts tutorial