#python#usda#tutorial#sqlite#calorie-tracker

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 food and 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 today for 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