Programming with Python › Control Flow and Collections › Day 55
Hands-on lab — Day 55: Comprehensions and Iterator Thinking
- ← Back to the Day 55 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-055-comprehensions-and-iterator-thinking/
Commands
Setup
cd labs/sections/programming-with-python/day-055-comprehensions-and-iterator-thinking
python3 --version Run
python3 examples/pipeline.py
python3 starter/pipeline.py Test
bash tests/run_tests.sh File tree
examples/pipeline.py expected-output/FIELDS.md expected-output/sample-run.txt expected-output/test-run.txt metadata.yml README.md requirements/README.md security.md starter/pipeline-worksheet.md starter/pipeline.py tests/run_tests.sh troubleshooting.md
Lab README
Day 055 lab — Comprehensions & Generators
Lesson
- Lesson title: Comprehensions and Iterator Thinking
- Day number: 55 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-055-comprehensions-and-iterator-thinking
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-055-comprehensions-and-iterator-thinkingwhen the site is running.
Purpose
Day 55's lesson teaches two idioms at once: comprehensions (the everyday
one-line transform) and iterator thinking (processing a stream lazily, one
item at a time). This lab makes both concrete. You build list, dict, and
set comprehensions that map and filter a small set of records, then a
lazy generator pipeline that streams those records through a reader and a
filter and computes an aggregate — and you prove the lazy pipeline returns
exactly the same answer as a plain for loop. It is the shape every AI data
loader, streaming tokenizer, and batch pipeline is built on, shrunk to six
rows you can read.
Learning objectives
- Write list, dict, and set comprehensions that map and filter in one line.
- Write a
yield-based generator function and explain why it is lazy. - Assemble a generator pipeline (reader → filter → aggregate) that holds only one record in memory at a time.
- Prove a lazy pipeline is equivalent to an eager loop with an
assert. - Use
itertools(count,islice) to take a slice of an endless stream.
Prerequisites
- The Day 55 lesson (read it first — it explains every part this lab builds).
- Days 51-54: loops, and lists, dictionaries, tuples, and sets.
- A text editor and a terminal. No experience beyond this week is assumed.
Supported operating systems
- macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14).
- Linux — fully supported (any distribution with Python 3 and bash).
- Windows — use WSL and follow the Linux path, or substitute
pythonforpython3if that is how Python is exposed. The program is pure standard-library Python and behaves identically everywhere.
Hardware requirements
Any computer that runs Python 3. The program transforms six small records in memory; it needs no special memory, disk, or GPU.
Required software
python3(3.8 or newer; tested on 3.14).bashfor the test runner (preinstalled on macOS and Linux).- Standard library only — the
sysanditertoolsmodules ship with Python. No packages to install. Seerequirements/README.md.
Free and open-source options
Everything here is free and open source: Python, bash, and the standard
library. No account, API key, network access, or purchase is needed.
Comprehensions, generators, and itertools are core language features.
Installation
None beyond Python itself. Move into this directory and you are ready:
cd labs/sections/programming-with-python/day-055-comprehensions-and-iterator-thinking
python3 --version # confirm Python 3.8+ is available
File structure
day-055-comprehensions-and-iterator-thinking/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── pipeline.py ← YOUR working file (5 numbered exercises)
│ └── pipeline-worksheet.md ← design a second pipeline before coding it
├── examples/
│ └── pipeline.py ← complete reference implementation
├── tests/
│ └── run_tests.sh ← assert-based checks (comprehensions + pipeline)
├── expected-output/
│ ├── sample-run.txt ← real captured run of the reference
│ ├── test-run.txt ← real captured run of the test suite
│ └── FIELDS.md ← required behaviour on every platform
├── requirements/
│ └── README.md ← dependency statement (Python 3 only)
├── troubleshooting.md
└── security.md
How to run
From this directory:
## 1. See the finished program first
python3 examples/pipeline.py
## 2. Your task: complete the five exercises in the starter, then run it
python3 starter/pipeline.py
## 3. Prove laziness: pull ONE record from the reader without consuming the rest
python3 -c "import sys; sys.path.insert(0, 'examples'); from pipeline import read_records, RECORDS; g = read_records(RECORDS); print(next(g))"
## 4. Check your work
bash tests/run_tests.sh
What the commands do
python3 examples/pipeline.py— runs the complete reference: it prints the results of the list, dict, and set comprehensions, the first three ids from anitertoolscounter, and the engineering average computed two ways — a lazy generator pipeline and an explicit loop — then asserts the two match and printsmatch: lazy pipeline == loop baseline.python3 starter/pipeline.py— runs your version. The starter ships with five exercises stubbed out (each raisingNotImplementedErroruntil you finish it): a list comprehension, a dict comprehension, a set comprehension, ayield-based generator, and the assembled lazy pipeline.python3 -c "... next(g) ..."— pulls a single record from the reader generator withnext(), printing just one record dict. This proves the generator produces items on demand rather than building them all first.bash tests/run_tests.sh— imports the reference module and asserts the return values of every comprehension and generator, runs the whole program and confirms its self-check, and checks your starter (structurally until you finish, strictly afterwards). Exits 0 only if every check passes.
Expected output
See expected-output/sample-run.txt — a
real captured run:
$ python3 examples/pipeline.py
high scorers (list): ['ALICE', 'CAROL', 'FRANK']
name -> score (dict): {'alice': 88, 'bob': 72, 'carol': 95, 'dave': 60, 'erin': 79, 'frank': 84}
distinct teams (set): ['design', 'engineering', 'marketing']
first 3 ids (itertools): [1000, 1001, 1002]
engineering average (lazy pipeline): 87.3
engineering average (loop baseline): 87.3
match: lazy pipeline == loop baseline
The program is deterministic, so your output will match exactly (the set is
printed sorted so it is stable).
expected-output/FIELDS.md lists the required
behaviour for every line on every platform.
Validation steps
- Run
python3 examples/pipeline.py— it must print the three comprehension results and end withmatch: lazy pipeline == loop baseline. - Complete the five exercises in
starter/pipeline.py, then run it and confirm it matches the reference. - Run the
next(g)one-liner — it must print exactly one record dict. - Run the tests (next section) — every check must pass.
Tests
bash tests/run_tests.sh
Expected final line: 12 checks, 0 failure(s). The checks are assert-based:
they import the module and assert the return value of each comprehension and
generator, run the whole program, and check your starter. The command exits 0
on success and non-zero on any failure, so it can run in CI. A full captured
run is in expected-output/test-run.txt.
Cleanup
Nothing to clean up: the program and tests read only their own in-memory data
and write nothing outside their console output (no files, no network, no
settings). To reset your work, restore the starter from git:
git checkout -- starter/pipeline.py.
Troubleshooting
See troubleshooting.md for the full list: python vs
python3, the deliberate NotImplementedError stubs, indexing a generator,
unordered sets, exhausted generators, and keeping only_team lazy.
Security notes
See security.md. Short version: the program makes no network
calls, writes no files, and needs no privileges. A comprehension runs real
code for every item, so keep its output expression a plain transform and
never eval() untrusted text — the same rule you learned for input handling.
Extension exercises
- Write a generator
batched(iterable, size)that yields tuples of up tosizeitems — the exact shape of a model's data loader — and confirm it streams without building the full list. - Put a
print(f"reading {r['name']}")inside the reader, wrap it initertools.islice(reader, 2), and confirm only two "reading" lines appear — proving records beyond the second were never read. - Use
itertools.chainto splice two record streams into one and run a single comprehension over the combined stream, showing the consumer neither knows nor cares that the data came from two sources.
Navigation
- Previous day: Day 54 — Tuples, Sets, and Choosing a Collection
(
labs/sections/programming-with-python/day-054-tuples-sets-and-choosing-a-collection/). - Next day: Day 56 — Building a Data-Driven CLI
(
labs/sections/programming-with-python/day-056-building-a-data-driven-cli/, to be written). - Week 8 project: the Terminal Task Manager, a to-do CLI built on lists and dictionaries — the same transform-and-filter habits you practise here.
Expected output
FIELDS.md
# Expected output — Day 055 lab
This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, Python 3.14, 2026-07-13). Your numbers will match
exactly, because the program is deterministic — the same input always
produces the same output on every platform where Python 3 runs.
## Files
- `sample-run.txt` — the reference program (`python3 examples/pipeline.py`)
plus the `python3 -c` one-liner that pulls a single record from the reader
generator with `next()`.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
still unfinished (12 checks, 0 failures). Absolute paths are shown as
`<repo>`; the runner itself prints no machine paths.
## Required behaviour on every platform
A correct program must produce exactly:
| Line | Value |
| ---- | ----- |
| high scorers (list) | `['ALICE', 'CAROL', 'FRANK']` |
| name -> score (dict) | `{'alice': 88, 'bob': 72, 'carol': 95, 'dave': 60, 'erin': 79, 'frank': 84}` |
| distinct teams (set) | `['design', 'engineering', 'marketing']` (printed sorted, so stable) |
| first 3 ids (itertools) | `[1000, 1001, 1002]` |
| engineering average (lazy pipeline) | `87.3` |
| engineering average (loop baseline) | `87.3` |
| final line | `match: lazy pipeline == loop baseline` |
The engineering average is `(88 + 95 + 79) / 3 = 262 / 3 = 87.333...`, shown
to one decimal place as `87.3`. The two averages are identical by design: the
lazy generator pipeline and the eager loop compute the same quantity, and an
`assert` in the program fails loudly if they ever diverge.
## Notes on non-determinism
- **Sets are unordered.** The distinct-teams line is printed through
`sorted(...)` precisely so it is stable to compare; comparing the raw
`set` repr across runs or Python versions is not reliable.
- **Dict order** is insertion order in Python 3.7+, so the `name -> score`
line is stable across every supported Python.
## Test-suite counts
- With the starter unfinished: `12 checks, 0 failure(s).`
- Once you complete all five starter exercises: still `12 checks, 0
failure(s).` — the two structural starter checks are replaced by two strict
checks that run your completed starter and assert its list comprehension,
so the total stays the same while the bar rises.
sample-run.txt
$ python3 examples/pipeline.py
high scorers (list): ['ALICE', 'CAROL', 'FRANK']
name -> score (dict): {'alice': 88, 'bob': 72, 'carol': 95, 'dave': 60, 'erin': 79, 'frank': 84}
distinct teams (set): ['design', 'engineering', 'marketing']
first 3 ids (itertools): [1000, 1001, 1002]
engineering average (lazy pipeline): 87.3
engineering average (loop baseline): 87.3
match: lazy pipeline == loop baseline
$ python3 -c "import sys; sys.path.insert(0, 'examples'); from pipeline import read_records, RECORDS; g = read_records(RECORDS); print(next(g))"
{'name': 'alice', 'age': 34, 'team': 'engineering', 'score': 88}
test-run.txt
Asserting comprehension results (examples/pipeline.py) ...
ok: list comp: high scorers upper-cased
ok: dict comp: name -> score
ok: set comp: distinct teams
Asserting generator behaviour ...
ok: read_records yields one record at a time
ok: only_team filters lazily and is a generator
ok: lazy pipeline == loop baseline (engineering avg)
ok: engineering average is 262/3
ok: itertools first_ids uses count+islice
Running the whole reference program ...
ok: reference program runs and self-checks (exit 0)
Testing starter/pipeline.py ...
ok: starter is valid Python
Note: starter/pipeline.py still has unfinished exercises — testing structure only.
ok: starter defines high_scorer_names
ok: starter uses yield somewhere
12 checks, 0 failure(s).
Source files
examples/pipeline.py (4074 bytes)
#!/usr/bin/env python3
"""Comprehensions and a lazy generator pipeline.
A complete, small, real program that demonstrates both halves of Day 55:
1. Comprehensions (list, dict, set) — the everyday transforms that map and
filter a collection in one readable line.
2. A lazy generator pipeline — reading, filtering, and aggregating a stream
of records one at a time, holding only a single record in memory, and
proving it returns the same answer as a plain eager loop.
Run it:
python3 examples/pipeline.py
It reads no files and takes no arguments: the "stream" is a small in-memory
list of rows standing in for a data source far too large to hold at once.
"""
import itertools
import sys
# A tiny in-memory "stream". In real life these rows would arrive from a huge
# file or a network socket, one at a time, far too large to hold in memory.
RECORDS = [
"alice,34,engineering,88",
"bob,29,design,72",
"carol,41,engineering,95",
"dave,38,design,60",
"erin,25,engineering,79",
"frank,52,marketing,84",
]
def parse(row):
"""Turn a raw 'name,age,team,score' row into a typed dict."""
name, age, team, score = row.split(",")
return {"name": name, "age": int(age), "team": team, "score": int(score)}
# --- Comprehensions: the everyday transforms -------------------------------
def high_scorer_names(records):
"""List comprehension: names of records scoring >= 80, upper-cased."""
return [r["name"].upper() for r in records if r["score"] >= 80]
def name_to_score(records):
"""Dict comprehension: map each name to its score."""
return {r["name"]: r["score"] for r in records}
def distinct_teams(records):
"""Set comprehension: the distinct teams present (order not kept)."""
return {r["team"] for r in records}
# --- Lazy generator pipeline -----------------------------------------------
def read_records(rows):
"""Generator: yield one parsed record at a time (O(1) memory)."""
for row in rows:
yield parse(row)
def only_team(records, team):
"""Generator: keep only records for one team, lazily."""
for r in records:
if r["team"] == team:
yield r
def scores(records):
"""Generator: yield just the score of each record."""
for r in records:
yield r["score"]
def average_score_lazy(rows, team):
"""Average score for a team via a lazy generator pipeline.
Each record is read, filtered, and consumed one at a time — the program
never holds more than a single record, however long `rows` is.
"""
pipeline = scores(only_team(read_records(rows), team))
total = 0
count = 0
for score in pipeline:
total += score
count += 1
return total / count if count else 0.0
def average_score_loop(rows, team):
"""The same answer computed eagerly with a plain loop (the baseline)."""
matching = []
for row in rows:
record = parse(row)
if record["team"] == team:
matching.append(record["score"])
return sum(matching) / len(matching) if matching else 0.0
def first_ids(n):
"""itertools.count + islice: the first n ids from an endless counter."""
return list(itertools.islice(itertools.count(1000), n))
def main(argv):
"""Print every result and assert the lazy pipeline matches the loop."""
team = "engineering"
print("high scorers (list): ", high_scorer_names([parse(r) for r in RECORDS]))
print("name -> score (dict): ", name_to_score([parse(r) for r in RECORDS]))
print("distinct teams (set): ", sorted(distinct_teams([parse(r) for r in RECORDS])))
print("first 3 ids (itertools):", first_ids(3))
lazy = average_score_lazy(RECORDS, team)
loop = average_score_loop(RECORDS, team)
print(f"{team} average (lazy pipeline): {lazy:.1f}")
print(f"{team} average (loop baseline): {loop:.1f}")
assert lazy == loop, "lazy pipeline must match the loop baseline"
print("match: lazy pipeline == loop baseline")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
metadata.yml (616 bytes)
lesson_id: D055
day: 55
kind: data-processing
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-055-comprehensions-and-iterator-thinking
- python3 --version
run_commands:
- python3 examples/pipeline.py
- python3 starter/pipeline.py
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/pipeline.py # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-13'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 12 checks, 0 failure(s), exit 0'
requirements/README.md (850 bytes)
# Dependencies — Day 055 lab
**Python 3 only. No third-party packages.**
- `python3` (3.8 or newer; tested on 3.14). You set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — the `sys` and `itertools`
modules, both of which ship with Python. There is deliberately no
`requirements.txt`: comprehensions and generators are core language
features that run on a plain Python install with nothing to install first.
Check your Python is present and new enough:
```bash
python3 --version
```
If that prints `Python 3.8` or higher, you are ready. Windows users: run the
commands inside WSL, or use `python` in place of `python3` if that is how
Python is exposed on your system. The program is pure standard-library
Python and behaves identically on every platform.
starter/pipeline-worksheet.md (1632 bytes)
# Pipeline design worksheet — Day 055
Design your second pipeline *before* you code it (practice assignment). Pick
a different set of records — books, transactions, songs — and plan the same
five pieces you built for the sample data. Keep this file; the Week 8 project
(Terminal Task Manager) reuses these transform-and-filter habits.
## 1. Your records
Write 5-8 sample rows and the fields each one has.
| Field | Type | Example |
| ----- | ---- | ------- |
| | | |
| | | |
| | | |
| | | |
## 2. List comprehension (map + filter)
- What do you keep (the `if`)? ____________________
- What do you build for each kept item (the output expression)? ____________________
- The one line:
```python
```
## 3. Dict comprehension (a lookup)
- Key: ____________________ Value: ____________________
- The one line:
```python
```
## 4. Set comprehension (distinct values)
- Which field's distinct values? ____________________
- The one line:
```python
```
## 5. Lazy generator pipeline
- Stage 1 — reader (`yield` one parsed record): ____________________
- Stage 2 — filter (`yield` only records that qualify): ____________________
- Aggregate computed at the end (sum or average of what?): ____________________
- The `assert` that proves the lazy result equals the plain-loop result:
```python
assert lazy_result == loop_result
```
## 6. itertools.islice
- Paste the line that prints just the first two items of your reader, and
what it printed:
```text
```
## 7. What the program printed (fill in after building)
```text
```
starter/pipeline.py (4417 bytes)
#!/usr/bin/env python3
"""Comprehensions and a lazy generator pipeline — YOUR working file.
Complete the five numbered exercises below. Each names exactly what to write.
The finished reference is in examples/pipeline.py — try each exercise
yourself before peeking.
When all five are done, this file prints the same output as the reference:
python3 starter/pipeline.py
Then run: bash tests/run_tests.sh
"""
import itertools
import sys
RECORDS = [
"alice,34,engineering,88",
"bob,29,design,72",
"carol,41,engineering,95",
"dave,38,design,60",
"erin,25,engineering,79",
"frank,52,marketing,84",
]
def parse(row):
"""Turn a raw 'name,age,team,score' row into a typed dict. (Provided.)"""
name, age, team, score = row.split(",")
return {"name": name, "age": int(age), "team": team, "score": int(score)}
# --- Comprehensions ---------------------------------------------------------
def high_scorer_names(records):
"""List comprehension: names of records scoring >= 80, upper-cased."""
# Exercise 1: LIST COMPREHENSION.
# Return a list of r["name"].upper() for each record r whose
# r["score"] is >= 80. One line: [ ... for r in records if ... ].
# Expected on the sample data: ['ALICE', 'CAROL', 'FRANK'].
raise NotImplementedError("Exercise 1: implement high_scorer_names")
def name_to_score(records):
"""Dict comprehension: map each name to its score."""
# Exercise 2: DICT COMPREHENSION.
# Return {r["name"]: r["score"] for r in records}. The key is the name,
# the value is the score.
raise NotImplementedError("Exercise 2: implement name_to_score")
def distinct_teams(records):
"""Set comprehension: the distinct teams present."""
# Exercise 3: SET COMPREHENSION.
# Return a set of each r["team"]. Duplicates collapse automatically.
raise NotImplementedError("Exercise 3: implement distinct_teams")
# --- Lazy generator pipeline ------------------------------------------------
def read_records(rows):
"""Generator: yield one parsed record at a time (O(1) memory). (Provided.)"""
for row in rows:
yield parse(row)
def only_team(records, team):
"""Generator: keep only records for one team, lazily."""
# Exercise 4: GENERATOR FUNCTION.
# Loop over records; for each r whose r["team"] == team, `yield r`.
# Use yield (not return, not append) so this stays lazy.
raise NotImplementedError("Exercise 4: implement only_team")
def scores(records):
"""Generator: yield just the score of each record. (Provided.)"""
for r in records:
yield r["score"]
def average_score_lazy(rows, team):
"""Average score for a team via a lazy generator pipeline."""
# Exercise 5: ASSEMBLE THE PIPELINE.
# Build: pipeline = scores(only_team(read_records(rows), team))
# Then loop over the pipeline, summing the scores and counting them,
# and return total / count (or 0.0 if count is 0). Each record is
# touched exactly once, one at a time.
raise NotImplementedError("Exercise 5: implement average_score_lazy")
def average_score_loop(rows, team):
"""The same answer computed eagerly with a plain loop. (Provided baseline.)"""
matching = []
for row in rows:
record = parse(row)
if record["team"] == team:
matching.append(record["score"])
return sum(matching) / len(matching) if matching else 0.0
def first_ids(n):
"""itertools.count + islice: the first n ids from an endless counter. (Provided.)"""
return list(itertools.islice(itertools.count(1000), n))
def main(argv):
"""Print every result and assert the lazy pipeline matches the loop."""
team = "engineering"
print("high scorers (list): ", high_scorer_names([parse(r) for r in RECORDS]))
print("name -> score (dict): ", name_to_score([parse(r) for r in RECORDS]))
print("distinct teams (set): ", sorted(distinct_teams([parse(r) for r in RECORDS])))
print("first 3 ids (itertools):", first_ids(3))
lazy = average_score_lazy(RECORDS, team)
loop = average_score_loop(RECORDS, team)
print(f"{team} average (lazy pipeline): {lazy:.1f}")
print(f"{team} average (loop baseline): {loop:.1f}")
assert lazy == loop, "lazy pipeline must match the loop baseline"
print("match: lazy pipeline == loop baseline")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
tests/run_tests.sh (5033 bytes)
#!/usr/bin/env bash
# Tests for the Day 055 lab. Run from the lab directory:
# bash tests/run_tests.sh
#
# The checks are assert-based: they import the reference module and assert the
# return values of the comprehensions and the generator pipeline, then run the
# whole program and confirm the lazy pipeline matches the loop baseline.
# Finally they check the learner's starter: structurally while exercises are
# unfinished, and to the same strict standard once they are complete.
# No network, non-interactive. Exits 0 only if every check passes.
set -u
# Keep the working tree clean: do not let imported modules write __pycache__.
export PYTHONDONTWRITEBYTECODE=1
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/pipeline.py"
starter="${lab_dir}/starter/pipeline.py"
examples_dir="${lab_dir}/examples"
failures=0
checks=0
check() {
local label="$1" ok="$2"
checks=$((checks + 1))
if [ "${ok}" = "yes" ]; then
echo " ok: ${label}"
else
echo " FAIL: ${label}"
failures=$((failures + 1))
fi
}
# assert_py <label> <python-code>
# Runs an assert-based snippet against the examples module; passes only if
# python exits 0 (every assert held).
assert_py() {
local label="$1" code="$2"
if python3 -c "import sys; sys.path.insert(0, '${examples_dir}'); ${code}" 2>/dev/null; then
check "${label}" "yes"
else
check "${label}" "no"
fi
}
echo "Asserting comprehension results (examples/pipeline.py) ..."
assert_py "list comp: high scorers upper-cased" \
"from pipeline import high_scorer_names, parse, RECORDS; recs=[parse(r) for r in RECORDS]; assert high_scorer_names(recs) == ['ALICE','CAROL','FRANK']"
assert_py "dict comp: name -> score" \
"from pipeline import name_to_score, parse, RECORDS; recs=[parse(r) for r in RECORDS]; assert name_to_score(recs)['carol'] == 95 and len(name_to_score(recs)) == 6"
assert_py "set comp: distinct teams" \
"from pipeline import distinct_teams, parse, RECORDS; recs=[parse(r) for r in RECORDS]; assert distinct_teams(recs) == {'engineering','design','marketing'}"
echo "Asserting generator behaviour ..."
assert_py "read_records yields one record at a time" \
"from pipeline import read_records, RECORDS; g=read_records(RECORDS); first=next(g); assert first['name']=='alice' and isinstance(first['score'], int)"
assert_py "only_team filters lazily and is a generator" \
"import types; from pipeline import only_team, read_records, RECORDS; g=only_team(read_records(RECORDS),'design'); assert isinstance(g, types.GeneratorType); assert [r['name'] for r in g] == ['bob','dave']"
assert_py "lazy pipeline == loop baseline (engineering avg)" \
"from pipeline import average_score_lazy, average_score_loop, RECORDS; assert average_score_lazy(RECORDS,'engineering') == average_score_loop(RECORDS,'engineering')"
assert_py "engineering average is 262/3" \
"from pipeline import average_score_lazy, RECORDS; assert abs(average_score_lazy(RECORDS,'engineering') - 262/3) < 1e-9"
assert_py "itertools first_ids uses count+islice" \
"from pipeline import first_ids; assert first_ids(3) == [1000,1001,1002]"
echo "Running the whole reference program ..."
out="$(python3 "${ref}" 2>&1)"; code=$?
if [ "${code}" -eq 0 ] && printf '%s' "${out}" | grep -qF "match: lazy pipeline == loop baseline"; then
check "reference program runs and self-checks (exit 0)" "yes"
else
check "reference program runs and self-checks (exit 0)" "no"
echo " (exit ${code}; output: ${out})"
fi
# --- Learner starter ---
echo "Testing starter/pipeline.py ..."
if python3 -c "compile(open('${starter}').read(), '${starter}', 'exec')" 2>/dev/null; then
check "starter is valid Python" "yes"
else
check "starter is valid Python" "no"
fi
if grep -q 'NotImplementedError' "${starter}"; then
echo "Note: starter/pipeline.py still has unfinished exercises — testing structure only."
grep -q 'def high_scorer_names' "${starter}" && check "starter defines high_scorer_names" "yes" || check "starter defines high_scorer_names" "no"
grep -q 'yield' "${starter}" && check "starter uses yield somewhere" "yes" || check "starter uses yield somewhere" "no"
else
# Learner finished: hold the starter to the same strict standard.
s_out="$(python3 "${starter}" 2>&1)"; s_code=$?
if [ "${s_code}" -eq 0 ] && printf '%s' "${s_out}" | grep -qF "match: lazy pipeline == loop baseline"; then
check "completed starter runs and self-checks (exit 0)" "yes"
else
check "completed starter runs and self-checks (exit 0)" "no"
echo " (exit ${s_code}; output: ${s_out})"
fi
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/starter'); from pipeline import high_scorer_names, parse, RECORDS; recs=[parse(r) for r in RECORDS]; assert high_scorer_names(recs) == ['ALICE','CAROL','FRANK']" 2>/dev/null; then
check "completed starter list comp is correct" "yes"
else
check "completed starter list comp is correct" "no"
fi
fi
echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 055 lab
python: command not found
Use python3 explicitly, as every command in this lab does. On macOS and
most Linux systems, bare python may be missing or point to an old version.
Check with python3 --version.
The starter raises NotImplementedError when I run it
That is expected until you finish the exercises. Each unfinished function
raises NotImplementedError on purpose so you cannot accidentally think an
empty function "works." Replace each raise NotImplementedError(...) line
with the real body described in the comment above it. Once all five
exercises are done, the file prints exactly what the reference does.
TypeError: 'generator' object is not subscriptable
You tried to index a generator, like gen[0]. Generators have no indexing
and no length — they only produce items on demand. Use next(gen) to pull a
single item, or list(gen) to materialize them all into a list (only safe
when the stream is small enough to fit in memory).
My set line prints in a different order each run
Sets are unordered, so their repr is not stable to compare. The reference
prints the set through sorted(...) for exactly this reason. If you compare
distinct-teams output, sort it first.
My pipeline prints nothing, or a wrong average
A generator is single-use: after one full pass it is exhausted and yields
nothing more. If you consumed the pipeline once already — for example in a
debug print(list(pipeline)) — rebuild it before the real computation.
Assemble the pipeline fresh (scores(only_team(read_records(rows), team)))
immediately before you loop over it.
only_team returns a list instead of streaming
If you wrote return [...] or built a list with append, the function is no
longer lazy — it materializes everything. Use yield r inside the loop so
the function becomes a generator that hands out one record at a time. The
test checks that only_team(...) is a real generator object.
bash: tests/run_tests.sh: Permission denied
Run it through bash explicitly (as the README shows) rather than executing it
directly: bash tests/run_tests.sh. You do not need to chmod +x anything.
The two averages do not match, and the program crashes on the assert
That is the program catching a real bug for you. The lazy pipeline and the
loop baseline must compute the same number; if the assert fails, one of
them filters or sums differently. Re-check that only_team keeps exactly the
records whose team matches, and that average_score_lazy divides the total
by the count of records it actually saw.
Security notes
Security notes — Day 055 lab
-
What the program does: transforms a small in-memory list of records with comprehensions and streams it through a lazy generator pipeline, printing results. It makes no network connections, reads and writes no files, and changes no settings. The test runner is equally self-contained and non-interactive.
-
A comprehension runs real code for every item. The output expression of a comprehension is evaluated once per kept item, so never build one whose expression executes untrusted text. Keep the output a plain transform (
r["name"].upper(),int(x)), never a call that runs a string a user supplied. There is noeval()here, and there must never be one — the same rule you learned for input handling applies inside comprehensions. -
Laziness delays errors — validate where you consume. Because a generator computes on demand, an exception inside it surfaces when you pull on it, not when you create it. Validate inputs at the point they are read (inside the reader generator), and do not let a half-consumed generator swallow errors silently.
-
Streaming minimizes data held in memory. A generator pipeline touches each record once and forgets it, so a filter that drops fields early means sensitive data never accumulates. Building a giant list first does the opposite. Prefer streaming with an early filter when records could contain anything private.
-
Privileges: everything runs as your normal user. Nothing here needs
sudo. As always in this course, if a script ever asks you to run it with elevated privileges, stop and read it first. -
Reading before running: every file in this lab is short and commented. Read
examples/pipeline.pyandtests/run_tests.shbefore running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.