Programming with PythonControl Flow and Collections › Day 55

Hands-on lab — Day 55: 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

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 python for python3 if 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).
  • bash for the test runner (preinstalled on macOS and Linux).
  • Standard library only — the sys and itertools modules ship with Python. No packages to install. See requirements/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 an itertools counter, and the engineering average computed two ways — a lazy generator pipeline and an explicit loop — then asserts the two match and prints match: lazy pipeline == loop baseline.
  • python3 starter/pipeline.py — runs your version. The starter ships with five exercises stubbed out (each raising NotImplementedError until you finish it): a list comprehension, a dict comprehension, a set comprehension, a yield-based generator, and the assembled lazy pipeline.
  • python3 -c "... next(g) ..." — pulls a single record from the reader generator with next(), 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

  1. Run python3 examples/pipeline.py — it must print the three comprehension results and end with match: lazy pipeline == loop baseline.
  2. Complete the five exercises in starter/pipeline.py, then run it and confirm it matches the reference.
  3. Run the next(g) one-liner — it must print exactly one record dict.
  4. 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

  1. Write a generator batched(iterable, size) that yields tuples of up to size items — the exact shape of a model's data loader — and confirm it streams without building the full list.
  2. Put a print(f"reading {r['name']}") inside the reader, wrap it in itertools.islice(reader, 2), and confirm only two "reading" lines appear — proving records beyond the second were never read.
  3. Use itertools.chain to 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.
  • 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 no eval() 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.py and tests/run_tests.sh before 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.