Build a Reusable Picnic List

PythonBeginner
Practice Now

Introduction

Friends are planning a picnic. Two recipes share ingredients, some supplies are already at home, and the shop sells whole packages. Your job is to finish a reusable Python program that turns those facts into a shopping list you can actually use.

You will first run a small working case, then improve the prepared calculation module and create a complete CSV. The command-option and CSV-writing code in picnic.py, together with synthetic recipes, is provided. This lab assumes Python lists, dictionaries, loops, functions and basic exceptions, plus basic terminal input. The new file formats and options are taught here. The estimated time includes reading and ordinary errors; it is an author estimate, not a measured beginner completion time.

Read a Small Picnic List

In this step, you will generate a three-person shopping list for one salad and connect each CSV row to its JSON input. Begin in WebIDE. Open its Terminal menu and choose New Terminal. All commands in this lab go in that terminal. The prepared files are independent of any earlier lab.

You need Python lists, dictionaries, loops, functions and basic exceptions. JSON files, CSV writing and the command options used here are explained below. This is an integer-quantity exercise: grams (g), milliliters (ml) or item counts (each), with one unit per ingredient ID. It does not convert units or plan nutrition.

Start from a working small case

cd changes the current directory. An absolute path begins at /; a later relative path such as data/menu.json is resolved from the current directory. Return here with the same cd if a path fails after you move elsewhere.

cd /home/labex/project/picnic

cat prints a text file. Read the small menu:

cat data/sample-menu.json

JSON uses {} for objects (loaded as Python dictionaries), [] for lists and double quotes for string keys. Each recipe has an ingredients object: "chickpea": 70 means 70 g per person, not 70 packages. chickpea is a stable ID, while a product's name is a display label. Two products may share a name without being interchangeable.

cat data/products.json

The product table supplies name, unit and a positive integer pack_size. Chickpeas come in 200 g packages. The small case uses data/empty-pantry.json, an empty object {}, meaning no stock. Other provided input quantities are nonnegative integers. A missing pantry ID also means zero stock; a missing product is an error because its package size is unknown.

The provided picnic.py code handles command options and CSV writing. python3 runs it. Options beginning with -- name the next value: the menu, pantry, product table, positive number of people, and output CSV path. --output names a file to create or replace, so use the new filenames shown here. The source JSON files stay unchanged.

python3 picnic.py --menu data/sample-menu.json --pantry data/empty-pantry.json --products data/products.json --people 3 --output sample.csv

Expected message:

Created 2 ingredient rows in sample.csv
cat sample.csv

The header names each column. Chickpeas require 70 * 3 = 210 g. Two 200 g packages supply 400 g, leaving 190 g. Tomatoes require 120 g, so one 250 g package leaves 130 g. This small case works, but the starter has two bugs that a shared menu and existing stock will expose.

The final CSV has one row per required ingredient ID and these columns: ingredient_id,name,unit,required,stock,shortfall,packs,leftover. stock is the original stock, not just the amount used. leftover includes unused original stock. Row and column order do not matter, but every field and relationship must be correct. Keep picnic.py, a reusable calculation program, and all original files in data/. You may use equivalent Python calculations; the interface and input/output formats stay the same.

Combine Shared Ingredients by ID

In this step, you will fix the shared requirement calculation before making a purchase decision. Continue in the same WebIDE terminal.

cat data/menu.json

The salad uses 70 g of chickpeas per person and the wraps use 40 g. For five people, the total must be (70 + 40) * 5 = 550 g. Buying separately for each recipe can overbuy packages. First combine all requirements, then deduct pantry stock once and round the remaining shortage once.

cat calculations.py

In total_requirements, the starter assigns amount * people to the ID each time. When the second recipe uses the same ID, it replaces the first recipe's requirement. totals.get(ingredient_id, 0) instead reads the total already accumulated, or zero when the ID is new. Add the next recipe's scaled amount to that value.

Replace the small calculation module

The following command writes the full module so indentation and function boundaries stay clear. cat > calculations.py redirects the text into that file, replacing its contents. <<'PYCODE' starts a here-document: paste everything through the final PYCODE line. Quoting the delimiter keeps the shell from interpreting the Python text. This operation replaces only calculations.py; it leaves your JSON and CSV files alone. If a typo occurs, rerun the complete block. Python indentation matters, so preserve the four-space blocks.

cat > calculations.py <<'PYCODE'
"""The two calculation functions you will improve."""


def total_requirements(menu, people):
    totals = {}
    for recipe in menu["recipes"]:
        for ingredient_id, amount in recipe["ingredients"].items():
            totals[ingredient_id] = totals.get(ingredient_id, 0) + amount * people
    return totals


def shopping_rows(totals, pantry, products):
    rows = []
    for ingredient_id, required in totals.items():
        product = products[ingredient_id]
        size = product["pack_size"]
        packs = (required + size - 1) // size
        rows.append({"ingredient_id": ingredient_id,
                     "name": product["name"], "unit": product["unit"],
                     "required": required, "stock": 0,
                     "shortfall": required, "packs": packs,
                     "leftover": packs * size - required})
    return rows
PYCODE

Only the aggregation is corrected so far. Generate the main five-person list:

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
cat five.csv

The chickpea required field must now be 550; tomatoes require 200 and wraps require 5. The stock column is still zero because the second function ignores the pantry. Do not treat this intermediate list as ready for shopping.

Buy Whole Packages After Using Stock

In this step, you will finish the shopping calculation and read a list that accounts for what is already at home. Continue in the WebIDE terminal.

cat data/pantry.json

For five people, chickpeas need 550 g and the pantry has 150 g, so the shortage is 400 g. Two 200 g packages cover it exactly. Tomatoes need 200 g, but 250 g are already available: the shortage and purchase count are both zero, and 50 g remain. Wraps need 5, stock is 2, and the shortage of 3 requires one package of 6; the final remainder is 3.

Use max(0, required - stock) so surplus stock cannot create a negative purchase. For nonnegative integer shortage s and positive package size p, (s + p - 1) // p rounds up to a whole package; // is integer floor division. With 200 g packages, shortage 0 gives 0 packages, 200 gives 1, and 201 gives 2. This keeps exact boundaries correct without floating-point arithmetic.

The final remainder is stock + packs * pack_size - required. Calculating only packs * pack_size - shortfall would miss unused pantry stock when stock exceeds the requirement.

Replace the module with both completed functions. The here-document works as in the previous step.

cat > calculations.py <<'PYCODE'
"""Calculate requirements before deciding what to buy."""


def total_requirements(menu, people):
    totals = {}
    for recipe in menu["recipes"]:
        for ingredient_id, amount in recipe["ingredients"].items():
            totals[ingredient_id] = totals.get(ingredient_id, 0) + amount * people
    return totals


def shopping_rows(totals, pantry, products):
    rows = []
    for ingredient_id, required in totals.items():
        product = products[ingredient_id]
        stock = pantry.get(ingredient_id, 0)
        shortfall = max(0, required - stock)
        size = product["pack_size"]
        packs = (shortfall + size - 1) // size
        rows.append({"ingredient_id": ingredient_id,
                     "name": product["name"], "unit": product["unit"],
                     "required": required, "stock": stock,
                     "shortfall": shortfall, "packs": packs,
                     "leftover": stock + packs * size - required})
    return rows
PYCODE
python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
cat five.csv

Expected content (order may differ in an equivalent implementation):

ingredient_id,name,unit,required,stock,shortfall,packs,leftover
chickpea,Chickpeas,g,550,150,400,2,0
tomato,Tomatoes,g,200,250,0,0,50
wrap,Wraps,each,5,2,3,1,3

This is a usable shopping list: buy two chickpea packages, no tomatoes and one wrap package. Include zero-purchase rows so the reader can distinguish “already covered” from an ingredient accidentally omitted. Only IDs required by this menu need rows; unrelated pantry items do not need rows.

Reuse the Program and Recover from Input Errors

In this step, you will understand the provided file-writing code in picnic.py, reuse your calculations for another menu and recover from ordinary mistakes. Continue in the WebIDE terminal. No variables or previous shell sessions are required; each command passes its input paths explicitly.

cat picnic.py

argparse reads the named options and supplies --help; type=int rejects noninteger people counts, and the next check rejects zero or negative counts. load_json uses open(..., encoding="utf-8") and json.load to read complete structured objects. A with block closes a file when the block ends. The calculation module returns a list of row dictionaries.

All inputs and calculations are processed before the output file is opened. The missing-product check compares required IDs against product IDs and raises ValueError when any package information is missing. csv.DictWriter writes the named columns, a header and the rows. newline="" lets the CSV module handle line endings; UTF-8 preserves names. Do not replace CSV writing with comma joining: a future product name containing a comma must remain one field.

The provided picnic.py code catches file, JSON/value and missing-key errors, prints an explanatory message to standard error and exits nonzero. It does not promise protection against an interrupted disk write, but invalid people or missing product information must leave an existing output unchanged.

Read an expected failure and recover

The next two commands intentionally fail. An error is expected; your terminal remains usable. The first asks for zero people. It must report people must be a positive integer before replacing five.csv.

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/products.json --people 0 --output five.csv

The second misspells the menu filename. Expect Cannot make list: and a missing-file message, not an empty successful list. Fix a spelling mistake by checking the names in data/ and rerunning with the correct path.

python3 picnic.py --menu data/menus.json --pantry data/pantry.json --products data/products.json --people 5 --output five.csv
ls data

missing-products.json is an intentional incomplete product table. It lacks products needed by the menu. The next command must report missing product: and preserve the existing five.csv. Unknown packaging is not equivalent to zero stock or zero purchases.

python3 picnic.py --menu data/menu.json --pantry data/pantry.json --products data/missing-products.json --people 5 --output five.csv
cat five.csv

The successful five-person list must still match the previous step. Recover by choosing the complete product file, rather than changing your original input data.

Make a different shopping list

cat data/other-menu.json

This menu has only tomatoes and wraps. For four people, it needs 200 g of tomatoes and 8 wraps. The same program should use the original pantry without retaining ingredients from the last run.

python3 picnic.py --menu data/other-menu.json --pantry data/pantry.json --products data/products.json --people 4 --output other.csv
cat other.csv

Tomatoes have zero purchases and 50 g left. Wraps have shortage 6, exactly one package, and no remainder. There must be no chickpea row. Your five.csv, sample.csv, program and original JSON files remain available. A changed menu and a new output name let you keep both shopping plans. The checks read your actual outputs and run your unchanged program on temporary input cases; they do not create or repair your live lists.

Summary

You combined per-person recipe requirements by ingredient ID, deducted stock once, and rounded only the remaining shortage to whole packages. You produced actual CSV shopping lists, kept zero-purchase rows meaningful, reused the same program for another menu, and recovered from invalid input without replacing a good result.