What This Challenge Tests
3 minBy the end of this lesson you should be comfortable with:
- Reading nested data — a list of dictionaries where one field is itself a list.
- Filtering records by a field value, and projecting a single field out.
- Summing, counting and finding maxima across a list of dicts.
- Reshaping a list of dicts into a dict of lists (group-by).
- Using set operations to answer membership questions.
The Dataset
5 minSave this dataset in a new file inventory.py. Every challenge below uses it — don't change the data, just write the queries underneath.
# inventory.py — Adva Warehouse stock list products = [ {"sku": "P001", "name": "pencil", "stall": "stationery", "price": 0.50, "qty": 120, "tags": ["writing", "wood"]}, {"sku": "P002", "name": "eraser", "stall": "stationery", "price": 0.80, "qty": 60, "tags": ["writing"]}, {"sku": "P003", "name": "ruler 30cm", "stall": "stationery", "price": 1.20, "qty": 18, "tags": ["measuring", "plastic"]}, {"sku": "P004", "name": "sharpener", "stall": "stationery", "price": 1.50, "qty": 8, "tags": ["writing", "metal"]}, {"sku": "F101", "name": "apple", "stall": "fruit", "price": 1.00, "qty": 40, "tags": ["fresh", "snack"]}, {"sku": "F102", "name": "banana", "stall": "fruit", "price": 0.40, "qty": 90, "tags": ["fresh", "snack"]}, {"sku": "F103", "name": "durian", "stall": "fruit", "price": 8.50, "qty": 5, "tags": ["fresh"]}, {"sku": "D201", "name": "milo", "stall": "drinks", "price": 4.50, "qty": 25, "tags": ["chocolate", "hot"]}, {"sku": "D202", "name": "kopi", "stall": "drinks", "price": 3.00, "qty": 35, "tags": ["caffeine", "hot"]}, {"sku": "D203", "name": "teh tarik", "stall": "drinks", "price": 3.50, "qty": 22, "tags": ["caffeine", "hot", "milk"]}, {"sku": "D204", "name": "cendol", "stall": "drinks", "price": 5.00, "qty": 0, "tags": ["sweet", "cold"]}, ]
Eleven products, four fields each plus a nested list of tags. Skim it once, then crack on with the six tasks below.
For each task, try first without opening the hint. Compare with the hint only after a real attempt. Tasks 4-6 are stretch goals — finish 1-3 first, then come back if there's time.
Task 1 · The Headline Numbers
8 minPrint four headline numbers about the warehouse:
- Total number of distinct products (=
len(products)). - Total units in stock — sum of every
qty. - Total stock value — sum of
qty * pricefor every product. - How many distinct stalls there are. Hint: use
set().
Hint
print("Products :", len(products)) print("Units :", sum([p["qty"] for p in products])) value = 0 for p in products: value += p["qty"] * p["price"] print("Value RM :", value) stalls = set([p["stall"] for p in products]) print("Stalls :", len(stalls), stalls)
Three list comprehensions and one set — exactly the patterns from PY-L2-02, L2-06 and L2-08.
Task 2 · Out-of-Stock & Low-Stock Alerts
8 minTwo filters in one go:
- Print every product whose
qtyis exactly 0, in this format:OUT — D204 cendol. - Print every product with
0 < qty <= 10asLOW — F103 durian (5). - At the end, print
{ALERTS: n, OUT: x, LOW: y}— the three totals.
Hint
out = [p for p in products if p["qty"] == 0] low = [p for p in products if 0 < p["qty"] <= 10] for p in out: print("OUT —", p["sku"], p["name"]) for p in low: print("LOW —", p["sku"], p["name"], "(", p["qty"], ")") print({"ALERTS": len(out) + len(low), "OUT": len(out), "LOW": len(low)})
Python supports the maths-chain 0 < x <= 10 directly — same as x > 0 and x <= 10, but neater.
Task 3 · The Most Valuable Single Product
8 minFor every product, the value-in-stock is qty * price. Find the product with the highest such value, and print:
Top single line: F102 banana — RM 36.0 ( 90 units × 0.4 )
Scan the list and track the whole record.
Hint
top = products[0] top_v = top["qty"] * top["price"] for p in products: v = p["qty"] * p["price"] if v > top_v: top = p top_v = v print("Top single line:", top["sku"], top["name"], "— RM", top_v, " (", top["qty"], "units ×", top["price"], ")")
The trick from PY-L2-11: tracking the whole record instead of just the number lets you print every detail at the end.
Task 4 · Group Totals by Stall
8 minBuild a by_stall dict where the key is the stall name and the value is the total stock value for that stall. Print the dict, then the stall with the highest combined value.
Hint
by_stall = {} for p in products: stall = p["stall"] value = p["qty"] * p["price"] if stall not in by_stall: by_stall[stall] = 0 by_stall[stall] += value print(by_stall) top_stall = "" top_value = 0 for stall, v in by_stall.items(): if v > top_value: top_value = v top_stall = stall print("Top stall:", top_stall, "(RM", top_value, ")")
This is a tweak of the group-by recipe — instead of appending to a list per group, we're summing into a number per group. Same shape, different operation.
Task 5 · The Tag Index (Stretch)
8 minEvery product has a list of tags. Build by_tag — a dict mapping each tag to a list of product names that wear it. Then:
- Print the dict.
- Print the tag with the most products attached.
- Print all products that are tagged both "writing" AND "metal" — use a set intersection on
set(by_tag["writing"]) & set(by_tag["metal"]).
Hint
by_tag = {} for p in products: for tag in p["tags"]: if tag not in by_tag: by_tag[tag] = [] by_tag[tag].append(p["name"]) print(by_tag) top_tag = "" top_n = 0 for tag, names in by_tag.items(): if len(names) > top_n: top_n = len(names) top_tag = tag print("Top tag:", top_tag, "(", top_n, ")") both = set(by_tag["writing"]) & set(by_tag["metal"]) print("Writing AND metal:", both)
The inner for tag in p["tags"] walks the nested list — that's the moment a list-of-dicts-with-list becomes a real test of your loop muscle. Set intersection from PY-L2-09 then turns the "tagged with both" question into one line.
Task 6 · The Promo Generator (Stretch)
7 minBuild a function make_promo(min_qty=20) that returns a new list-of-dicts containing only the products with qty >= min_qty, each with their price discounted by 10%. The function shouldn't change the original products list.
Test it: call make_promo(), then make_promo(50). Print both.
Hint
def make_promo(min_qty=20): promo = [] for p in products: if p["qty"] >= min_qty: promo.append({ "sku": p["sku"], "name": p["name"], "price": round(p["price"] * 0.9, 2), "was": p["price"], "qty": p["qty"], }) return promo print(make_promo()) print() print(make_promo(50))
The function takes a default argument (from PY-L1-28). Each loop iteration builds a brand-new dict — that's how we avoid mutating the original products. round(x, 2) tidies the discounted price to 2 decimal places.
Putting It All Together · Inventory Inspector CLI
8 minSave inspector.py. Wrap every task above into a menu-driven program that the user can drive.
Inside a while True: loop, offer:
1Headlines (Task 1)2Stock alerts (Task 2)3Top single line (Task 3)4Group totals (Task 4)5Tag index (Task 5)6Promo (Task 6) — ask the user for themin_qtythreshold7Quit
Show one possible solution
# inspector.py — menu-driven inventory inspector products = [ # ... paste your inventory.py products list here ... ] def headlines(): print("Products :", len(products)) print("Units :", sum([p["qty"] for p in products])) value = sum([p["qty"] * p["price"] for p in products]) print("Value RM :", value) stalls = set([p["stall"] for p in products]) print("Stalls :", len(stalls), stalls) def alerts(): out = [p for p in products if p["qty"] == 0] low = [p for p in products if 0 < p["qty"] <= 10] for p in out: print("OUT —", p["sku"], p["name"]) for p in low: print("LOW —", p["sku"], p["name"], "(", p["qty"], ")") print({"ALERTS": len(out) + len(low), "OUT": len(out), "LOW": len(low)}) def top_line(): top = products[0] top_v = top["qty"] * top["price"] for p in products: v = p["qty"] * p["price"] if v > top_v: top, top_v = p, v print("Top single line:", top["sku"], top["name"], "— RM", top_v) def group_totals(): by_stall = {} for p in products: by_stall[p["stall"]] = by_stall.get(p["stall"], 0) + p["qty"] * p["price"] print(by_stall) def tag_index(): by_tag = {} for p in products: for tag in p["tags"]: if tag not in by_tag: by_tag[tag] = [] by_tag[tag].append(p["name"]) print(by_tag) def promo(min_qty): out = [] for p in products: if p["qty"] >= min_qty: out.append({"sku": p["sku"], "name": p["name"], "price": round(p["price"] * 0.9, 2), "was": p["price"]}) print(out) while True: print() print("1 headlines 2 alerts 3 top 4 stalls 5 tags 6 promo 7 quit") pick = input("Choose: ") if pick == "1": headlines() elif pick == "2": alerts() elif pick == "3": top_line() elif pick == "4": group_totals() elif pick == "5": tag_index() elif pick == "6": n = int(input("min_qty: ")) promo(n) elif pick == "7": print("Bye!") break else: print("Pick 1-7.")
Non-negotiables: every task wrapped in a named function, a menu loop, and at least one user-typed argument feeding into the promo function. Once it works, this file is a working CLI you could ship.
Recap
3 minTwelve lessons of collections, all on one page. Filter with a comprehension. Sum a field with sum([p["f"] for p in data]). Find the best record by tracking the whole dict. Group by a category with the if-key-not-in recipe — for lists, or for running totals. Reach for a set whenever you need membership or unique items, and lean on set operators (&, |, -, ^) to answer combination questions in one line. Wrap each query in a function and you've got a working report tool.
You've mastered the four core collection shapes. Tomorrow we step sideways: split, join, strip, find — the string methods that turn messy text into clean data. Real-world data hardly ever arrives pre-bucketed; you usually have to slice it out of a string first.
Homework
4 minRe-skin inspector.py for a topic you actually care about. Pick one of:
- A list of your favourite games — fields: name, platform, hours-played, genres (list).
- A list of movies — name, year, runtime, tags (list).
- A list of football players — name, club, goals, positions (list).
Re-run all six tasks against your new dataset. Don't change the queries — only the data. Bring your re-skinned file to class.
Sample · games_inspector.py (top half)
# games_inspector.py — same six tasks, different dataset games = [ {"sku": "G01", "name": "Stardew Valley", "platform": "PC", "price": 14.99, "qty": 1, "tags": ["farm", "indie", "cozy"]}, {"sku": "G02", "name": "Celeste", "platform": "Switch", "price": 19.99, "qty": 1, "tags": ["platformer", "indie"]}, {"sku": "G03", "name": "Hades", "platform": "PC", "price": 24.99, "qty": 1, "tags": ["roguelike", "indie"]}, {"sku": "G04", "name": "Hollow Knight", "platform": "Switch", "price": 14.99, "qty": 1, "tags": ["metroidvania", "indie"]}, # ... ] # Task 1 print("Games on shelf :", len(games)) print("Distinct platforms:", set([g["platform"] for g in games])) # Task 4 — group total spend by platform by_platform = {} for g in games: by_platform[g["platform"]] = by_platform.get(g["platform"], 0) + g["price"] print(by_platform)
Same shape — different topic. The point is that the query logic is reusable; only the field names and dataset change. Bring your file and we'll combine it with classmates' in the next class.