1 The Problem
We want a recipe manager: add recipes (each with ingredients and steps), search for recipes that use an ingredient, and scale quantities up or down for different serving sizes. It teaches working with nested data — records that themselves contain lists — and the operations over them.
2 How to Think About It
Think about the shape of a recipe, before any code:
3 The Build — explained part by part
Here is the complete recipe manager. Each part is explained below.
import json
import os
FILE = "recipes.json"
def load():
return json.load(open(FILE)) if os.path.exists(FILE) else []
def save(recipes):
json.dump(recipes, open(FILE, "w"), indent=2)
def search_by_ingredient(recipes, ingredient):
ingredient = ingredient.lower()
return [r for r in recipes
if any(ingredient in i["name"].lower() for i in r["ingredients"])]
def scale(recipe, new_servings):
ratio = new_servings / recipe["servings"]
scaled = []
for i in recipe["ingredients"]:
scaled.append({"name": i["name"], "amount": round(i["amount"] * ratio, 2)})
return {"name": recipe["name"], "servings": new_servings,
"ingredients": scaled, "steps": recipe["steps"]}
if __name__ == "__main__":
recipe = {
"name": "Pancakes", "servings": 2,
"ingredients": [{"name": "flour", "amount": 100},
{"name": "milk", "amount": 200}],
"steps": ["Mix", "Fry"],
}
doubled = scale(recipe, 4)
print(doubled["ingredients"])
ingredients is itself a list of dictionaries (name + amount). This nesting models real recipes faithfully.search_by_ingredient — the tricky part:
any(ingredient in i["name"].lower() for i in r["ingredients"]) checks if any ingredient in a recipe matches. We keep recipes where that is true. any() stops at the first match.scale — compute the
ratio of new servings to old, then multiply every ingredient amount by it, rounding to 2 decimals. Doubling servings doubles every amount.round(i["amount"] * ratio, 2) — keep amounts tidy (no 33.33333 grams).
scale returns a new recipe — it does not change the original, so you can scale the same recipe many ways.
any(... for i in r["ingredients"]) to search inside.4 Test & Prove Each Part
We test searching nested ingredients and scaling amounts, with a known recipe.
def search_by_ingredient(recipes, ingredient):
ingredient = ingredient.lower()
return [r for r in recipes
if any(ingredient in i["name"].lower() for i in r["ingredients"])]
def scale(recipe, new_servings):
ratio = new_servings / recipe["servings"]
scaled = [{"name": i["name"], "amount": round(i["amount"] * ratio, 2)}
for i in recipe["ingredients"]]
return {"name": recipe["name"], "servings": new_servings, "ingredients": scaled}
RECIPE = {"name": "Pancakes", "servings": 2,
"ingredients": [{"name": "flour", "amount": 100}]}
def test_search():
"""Search finds a recipe by ingredient."""
assert len(search_by_ingredient([RECIPE], "flour")) == 1
def test_scale_doubles():
"""Doubling servings doubles the amount."""
result = scale(RECIPE, 4)
assert result["ingredients"][0]["amount"] == 200
def test_original_unchanged():
"""Scaling does not change the original recipe."""
scale(RECIPE, 4)
assert RECIPE["ingredients"][0]["amount"] == 100
Run with pytest -v. The original-unchanged test is important: it proves scale returns a fresh recipe rather than mutating the input — a subtle bug that this test guards against.
5 The Interface
Input
search_by_ingredient(recipes, "flour")Output
[{"name": "flour", "amount": 200}] # doubled6 Run It & Automate It
python3 recipes.pySee a recipe scaled from 2 to 4 servings. Recipes save to
recipes.json.Jenkins runs the tests automatically — each line explained below.
[{'name': 'flour', 'amount': 200.0}, {'name': 'milk', 'amount': 400.0}]scale instead of editing in place.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Shopping list. Combine ingredients across several recipes. (Teaches: merging nested data.)
- Categories. Tag recipes and filter by meal type. (Teaches: another search axis.)
- Unit conversion. Convert grams to cups. (Teaches: combining with the converter project.)
any(), and transforming them without mutating the original. Real-world data is nested, so these skills apply everywhere. Related: Comprehensions, json.