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. Open the Terminal tab. 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.

Find the calculation calls

Before changing a calculation, see where the prepared program uses it:

cat picnic.py

For now, locate just these two lines in main; the option parser and file-writing details can wait until step 4:

        totals = total_requirements(menu, args.people)
        rows = shopping_rows(totals, pantry, products)

The lines are separated by the missing-product check in the real file. total_requirements receives the menu and number of people and returns the total required by each ingredient ID. shopping_rows receives those totals, the pantry and products, and returns the rows that the prepared CSV writer saves. The import near the top loads both functions from calculations.py. You will edit that small module, then run picnic.py again to observe the result; you do not need to rewrite the command interface.

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 Terminal tab.

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.

Edit the accumulation line in nano

You will change one line in total_requirements, leaving shopping_rows and the rest of the file in place. Before editing, predict the chickpea requirement: two recipes contribute 70 g and 40 g per person, so five people need 550 g. The current assignment remembers only the last recipe. The corrected line must read the earlier total before adding the next amount.

nano is a text editor inside the Terminal tab. Open the existing file from the current workspace:

nano calculations.py

The filename at the top should be calculations.py, with the two functions you just read. If the buffer is blank or says New File, you opened the wrong path: press Ctrl+X, choose N if asked to save, and return to the workspace with the step 1 cd command before reopening the real file. Do not save an empty replacement over the starter.

In nano, ^ in the shortcut bar means hold Ctrl while pressing the letter. Press Ctrl+W, type def total_requirements, then press Enter to find that function. Arrow keys move the cursor; they do not change the file. Move down to the assignment inside the inner for loop:

            totals[ingredient_id] = amount * people

Keep its existing indentation and the left side of =. Use the arrow keys and Delete or Backspace to replace only the right side with totals.get(ingredient_id, 0) + amount * people. The resulting line is:

            totals[ingredient_id] = totals.get(ingredient_id, 0) + amount * people

The line begins with 12 spaces, inside both loops. Python uses indentation to group statements: the function body uses four spaces, the outer loop body eight and the inner loop body twelve. Preserve the leading spaces; if you accidentally remove them, restore them with the Space key. Do not use tabs or move return totals into a loop. Its existing four-space indentation returns the dictionary only after all recipes have been visited.

Save by pressing Ctrl+O, press Enter to confirm the existing filename, then press Ctrl+X to leave nano. You should see the shell prompt again before entering the next command. The save changes only calculations.py; original JSON files and existing CSVs remain in place.

If you notice a mistake while still editing, correct the affected line before saving. To abandon unsaved changes, press Ctrl+X and answer N at the save prompt; then reopen the file. This discards only edits since the last save. After a save, reopen the file and fix the line rather than resetting the environment or replacing both functions.

If Python later reports SyntaxError or IndentationError, read the filename and line number in the error. Open nano calculations.py, use Ctrl+W to find the named function, then use the arrow keys to inspect the affected line and the line just before it. Restore the line shown above, its leading spaces, quotes or colon as appropriate; save with Ctrl+O, Enter, and exit with Ctrl+X. A path error instead calls for the earlier cd/ls recovery. Preserve the input JSON and any good CSV; neither error requires a VM reset.

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 Terminal tab.

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.

Edit only the purchase calculation

Keep your corrected total_requirements function. Reopen the same module:

nano calculations.py

Press Ctrl+W, search for def shopping_rows, and press Enter. Read the existing for ingredient_id, required in totals.items(): loop. Each iteration builds one row for one required ID. product already supplies that ID's package size. Insert these two lines immediately after product = products[ingredient_id], before the existing size line:

        stock = pantry.get(ingredient_id, 0)
        shortfall = max(0, required - stock)

Each new line begins with eight spaces, aligned with product and size. pantry.get(..., 0) reads the original stock for this ID, using zero if it is absent. shortfall is what still needs buying. Leave size = product["pack_size"] in place, and change the next line to round shortfall, rather than the full requirement:

        packs = (shortfall + size - 1) // size

Now keep the existing rows.append dictionary, including its ID, name, unit and required fields. Change only its stock, shortfall and leftover values so the three affected lines read:

                     "required": required, "stock": stock,
                     "shortfall": shortfall, "packs": packs,
                     "leftover": stock + packs * size - required})

These are continuation lines inside the existing braces and parentheses; keep their current alignment. Preserve the commas, quotes and final }). The rows.append statement remains inside the loop, while return rows stays outside it with four leading spaces. Do not delete and retype the unaffected function or change the module description at the top.

Read the edited loop once before saving: it looks up stock, derives a nonnegative shortfall, rounds that shortfall to packages, then records original stock and all remaining quantity. Press Ctrl+O, Enter, then Ctrl+X. The shell prompt returns. Run the same five-person case to see what changed:

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 read the provided option and file-writing details in picnic.py, reuse your calculations for another menu and recover from ordinary mistakes. Continue in the Terminal tab. 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.