Automating Meal Planning with CookXml and PythonMeal planning can save time, reduce food waste, and make healthy eating more consistent — but creating and managing a meal plan for a week or month quickly becomes repetitive. Automating the process with structured recipe data makes the task scalable and reproducible. This article shows how to automate meal planning using CookXml (a lightweight XML schema for recipes) and Python. You’ll learn how to model recipes in CookXml, parse them, filter and score recipes by constraints (dietary needs, prep time, ingredients), and generate weekly meal plans with shopping lists and export options.
What is CookXml?
CookXml is an XML-based format for representing recipes and their metadata: ingredients, quantities, steps, tags (e.g., vegan, gluten-free), prep/cook times, servings, and nutritional info. Using XML makes recipes both human-readable and machine-processable. CookXml can be validated against an XML Schema (XSD) so that recipe sets stay consistent.
A simple CookXml recipe might include elements like:
- title
- description
- servings
- prepTime, cookTime
- ingredients (with quantity and unit)
- steps
- tags
- nutrition (calories, macros)
Why automate meal planning?
Automation helps with:
- saving time when creating weekly menus
- ensuring dietary constraints are respected (allergies, preferences)
- balancing nutrition and variety
- generating consolidated shopping lists
- adapting plans to available ingredients or leftovers
Overall approach
- Define/collect recipes in CookXml.
- Parse CookXml files into Python objects.
- Enrich recipes with metadata (popularity, rating, seasonality).
- Define user constraints (dietary filters, daily calorie targets, max prep time).
- Score and select recipes per meal slot (breakfast, lunch, dinner, snacks) to satisfy constraints while maximizing variety and preferences.
- Generate a weekly plan and a combined shopping list.
- Export plan as PDF/CSV/ICal and sync with calendar (optional).
Example CookXml schema (conceptual)
Below is a conceptual example of a single recipe in CookXml. (This is illustrative; your actual schema may differ or include an XSD for validation.)
<?xml version="1.0" encoding="UTF-8"?> <recipe id="r001"> <title>Roasted Chickpea Buddha Bowl</title> <description>A nutritious bowl with roasted chickpeas, quinoa, and greens.</description> <servings>2</servings> <prepTime>15</prepTime> <cookTime>25</cookTime> <tags> <tag>vegan</tag> <tag>gluten-free</tag> <tag>high-protein</tag> </tags> <ingredients> <ingredient> <name>chickpeas</name> <quantity>1</quantity> <unit>can</unit> <notes>drained and rinsed</notes> </ingredient> <ingredient> <name>quinoa</name> <quantity>1</quantity> <unit>cup</unit> </ingredient> <!-- more ingredients --> </ingredients> <steps> <step>Preheat oven to 425°F (220°C).</step> <step>Toss chickpeas with spices and roast 20 minutes.</step> <step>Cook quinoa according to package directions.</step> <step>Assemble bowl with greens, quinoa, chickpeas, and dressing.</step> </steps> <nutrition> <calories>420</calories> <protein>18</protein> <fat>12</fat> <carbs>58</carbs> </nutrition> </recipe>
Parsing CookXml in Python
Use Python’s built-in ElementTree or lxml for robust XML handling. Example structure for a parser:
- Recipe class (title, servings, times, ingredients, steps, tags, nutrition)
- Loader that reads a folder of .xml files and returns a list of Recipe objects
Example using ElementTree:
import xml.etree.ElementTree as ET from dataclasses import dataclass from typing import List, Optional @dataclass class Ingredient: name: str quantity: Optional[float] unit: Optional[str] notes: Optional[str] @dataclass class Recipe: id: str title: str servings: Optional[int] prep_time: Optional[int] cook_time: Optional[int] tags: List[str] ingredients: List[Ingredient] steps: List[str] nutrition: dict def parse_recipe(xml_path: str) -> Recipe: tree = ET.parse(xml_path) root = tree.getroot() rid = root.attrib.get("id", "") title = root.findtext("title") servings = root.findtext("servings") prep = root.findtext("prepTime") cook = root.findtext("cookTime") tags = [t.text for t in root.findall("tags/tag")] ingredients = [] for ing in root.findall("ingredients/ingredient"): name = ing.findtext("name") qty = ing.findtext("quantity") unit = ing.findtext("unit") notes = ing.findtext("notes") quantity = float(qty) if qty else None ingredients.append(Ingredient(name, quantity, unit, notes)) steps = [s.text for s in root.findall("steps/step")] nutrition = {n.tag: n.text for n in root.findall("nutrition/*")} return Recipe(rid, title, int(servings) if servings else None, int(prep) if prep else None, int(cook) if cook else None, tags, ingredients, steps, nutrition)
Filtering recipes by constraints
Define constraint checks:
- dietary (tags must include/exclude)
- max prep/cook time
- ingredient availability (pantry)
- calorie/macros range
Example filter function:
def filter_recipes(recipes, include_tags=None, exclude_tags=None, max_total_time=None, pantry=None): def ok(r): if include_tags and not set(include_tags).issubset(set(r.tags)): return False if exclude_tags and set(exclude_tags).intersection(set(r.tags)): return False total_time = (r.prep_time or 0) + (r.cook_time or 0) if max_total_time and total_time > max_total_time: return False if pantry: for ing in r.ingredients: if ing.name.lower() in pantry.get('avoid_missing', []): return False return True return [r for r in recipes if ok(r)]
Scoring and selecting recipes
To create a balanced plan, score recipes against user preferences and constraints. Consider:
- variety: penalize repeats of main ingredients or tags within the week
- prep time: prefer quick recipes on busy days
- user rating/popularity: favor highly rated recipes
- nutrition: select meals that help meet calorie/macros goals for the day
Example scoring formula (simplified):
Score® = w_rating * normalized_rating + w_time * (1 – time / max_time) + w_var * novelty_score + w_nutrition * nutrition_match
Implement greedy selection or an optimization approach (ILP) if you need strict nutritional constraints.
Weekly plan generation
Divide days into slots (breakfast, lunch, dinner, snack). For each slot:
- Filter recipes appropriate for the slot (breakfast-tagged, quick, etc.).
- Score remaining recipes for that day considering what was already chosen (to maximize variety).
- Select top-scoring recipe and mark its ingredients as planned.
Simple greedy scheduler example:
def plan_week(recipes, days=7, slots=['breakfast','lunch','dinner']): plan = {d: {s: None for s in slots} for d in range(days)} used_titles = set() for day in range(days): for slot in slots: candidates = [r for r in recipes if slot in r.tags] candidates = [r for r in candidates if r.title not in used_titles] if not candidates: candidates = [r for r in recipes if slot in r.tags] # naive score: prefer unrated/unused recipes chosen = max(candidates, key=lambda r: (r.title not in used_titles, -((r.prep_time or 0)+(r.cook_time or 0)))) plan[day][slot] = chosen used_titles.add(chosen.title) return plan
Shopping list generation
Consolidate ingredients across the week, convert units where possible, and aggregate quantities. Address unit normalization (cups, grams) and scaling by servings.
Key steps:
- normalize ingredient names (singular/plural, synonyms)
- convert units to a canonical system (metric or imperial) using a unit library (pint)
- multiply quantities by servings ratio if recipe servings differ from target
- sum quantities for identical items
Example pseudo-aggregation:
from collections import defaultdict shopping = defaultdict(lambda: {'qty': 0, 'unit': None}) for day in plan: for slot in plan[day]: r = plan[day][slot] for ing in r.ingredients: key = normalize_name(ing.name) qty = ing.quantity or 1 unit = ing.unit or 'count' # convert qty/unit to canonical form here shopping[key]['qty'] += qty shopping[key]['unit'] = unit
Consider flagging items that are commonly pantry staples (salt, pepper, oil) so they don’t clutter the list unless missing.
Handling allergies, preferences, and leftovers
- Tag recipes comprehensively (e.g., contains-nuts, dairy-free).
- Allow users to mark disliked ingredients; filter them out.
- Support leftovers by carrying over planned servings to future days and reducing required cooking.
- Allow recipe swaps within the plan if a key ingredient is unavailable.
Exporting plans
Common export options:
- CSV: meal plan + shopping list
- PDF: nicely formatted week view
- iCal/Google Calendar: place each meal as an event
- Mobile app sync (if you have an app)
Tools/libraries:
- pandas for CSV/Excel export
- reportlab or WeasyPrint for PDFs
- icalendar for iCal files
- Flask/FastAPI for a web UI
Example: Putting it together (mini CLI)
Create a CLI that:
- loads recipes from folder
- asks for dietary preferences and days to plan
- creates plan and shopping list
- outputs CSVs and a printable PDF
Core flow:
- python cookxml_loader.py –recipes ./recipes –days 7 –exclude nuts –max-time 45
- program prints summary and saves plan.csv and shopping.csv
Tips and best practices
- Maintain a normalized ingredient taxonomy to improve aggregation accuracy.
- Keep CookXml recipes validated with an XSD to avoid parsing errors.
- Allow users to rate recipes and track history to surface favorites.
- Cache parsed recipes to speed up repeated planning runs.
- Start simple: a greedy algorithm works well for most home users; move to optimization once you need strict nutrient balancing.
Next steps and extensions
- Integrate with grocery delivery APIs to order items automatically.
- Add seasonal/price-aware scoring using local produce calendars or price feeds.
- Use machine learning to predict which recipes a user will enjoy based on past ratings.
- Add voice or chat interfaces to modify plans conversationally.
Automating meal planning with CookXml and Python reduces friction and keeps meals varied and aligned with user needs. With a solid schema, a reliable parser, and flexible scoring/selection logic, you can build an effective system that saves time and improves nutrition.
Leave a Reply