Programming with PythonControl Flow and Collections › Day 51

Hands-on lab — Day 51: Loops: for, while, and Iteration Patterns

Commands

Setup

cd labs/sections/programming-with-python/day-051-loops-for-while-and-iteration-patterns
python3 --version

Run

python3 examples/patterns.py demo
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py total
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py filter 4
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 5

Test

bash tests/run_tests.sh

File tree

examples/patterns.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/patterns-worksheet.md
starter/patterns.py
tests/run_tests.sh
troubleshooting.md

Lab README

Day 051 lab — Iteration Patterns Workbench

Lesson

Purpose

Day 51's lesson teaches iteration from first principles. This lab makes the five core loop patterns concrete in one small, real program — the Iteration Patterns Workbench. It reads whole numbers from the command line or standard input and demonstrates each pattern in turn: accumulate a total and count, filter a sequence, transform each item, search with an early break, and build a small text histogram. You complete it from a starter, one pattern at a time, then run an automated test suite that checks real behaviour — output and exit code. These are the exact patterns that reappear in AI training loops, data-cleaning pipelines, and agent step-loops.

Learning objectives

  • Write the five iteration patterns as for loops: accumulate, filter, transform, search, and (in the histogram) nested iteration.
  • Use an early break (via an immediate return) to stop a search as soon as it succeeds, and measure the work saved.
  • Use enumerate to get index and value together without a hand-managed counter.
  • Validate input at the boundary so a bad token prints a clear error and exits non-zero, never a raw traceback.
  • Read data from standard input so the program is non-interactive and testable.

Prerequisites

  • The Day 51 lesson (read it first — it explains every pattern this lab builds).
  • Days 43-50: a working Python 3 install plus variables, strings, numbers, input/output, debugging, program structure, and conditionals.
  • A text editor and a terminal. No programming experience beyond this week is assumed.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14.0).
  • 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 does only small integer arithmetic and list building; it needs no special memory, disk, or GPU.

Required software

  • python3 (3.8 or newer; tested on 3.14.0).
  • bash for the test runner (preinstalled on macOS and Linux).
  • Standard library only — the sys module ships 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.

Installation

None beyond Python itself. Move into this directory and you are ready:

cd labs/sections/programming-with-python/day-051-loops-for-while-and-iteration-patterns
python3 --version   # confirm Python 3.8+ is available

File structure

day-051-loops-for-while-and-iteration-patterns/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── patterns.py                 ← YOUR working file (5 numbered exercises)
│   └── patterns-worksheet.md       ← design a sixth pattern before coding it
├── examples/
│   └── patterns.py                 ← complete reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (all patterns, good + bad input, imports)
├── 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 all five patterns at once on a built-in sample (no input needed)
python3 examples/patterns.py demo

## 2. Feed your own numbers to individual patterns via standard input
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py total
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py filter 4
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py transform
echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 5
echo "1 2 2 3 3 3"     | python3 examples/patterns.py histogram

## 3. Your task: complete the five exercises in the starter, then run it
echo "3 1 4 1 5 9 2 6" | python3 starter/patterns.py total

## 4. Prove the module is importable (the payoff of the main guard)
python3 -c "import sys; sys.path.insert(0, 'examples'); from patterns import accumulate_total; print(accumulate_total([3,1,4,1,5,9,2,6]))"

## 5. Check your work
bash tests/run_tests.sh

What the commands do

  • python3 examples/patterns.py demo — runs every pattern on the built-in sample list [3, 1, 4, 1, 5, 9, 2, 6] and prints a five-line report plus a histogram. It needs no input, so it is the quickest way to see the program.
  • echo "..." | python3 examples/patterns.py <command> — pipes numbers to a single pattern. total accumulates a count and sum; filter N keeps items greater than N; transform squares each item; search N scans with an early break and reports how many comparisons it made; histogram counts each distinct value and prints a bar of #. Bad input (a non-number token, an unknown command, or a missing argument) prints a clear error to standard error and exits with code 1.
  • python3 starter/patterns.py ... — runs your version. The starter ships with five pattern functions stubbed out (each raising NotImplementedError until you finish it): accumulate, filter, transform, search-with-break, and histogram.
  • python3 -c "...accumulate_total..." — imports one function from the module and calls it without running the whole program. This works only because the main guard holds main back on import.
  • bash tests/run_tests.sh — runs the reference on every pattern (good and bad inputs, checking output and exit code), imports two functions to check their return values, 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/patterns.py demo
sample: [3, 1, 4, 1, 5, 9, 2, 6]
total    -> count=8 sum=31
filter>4 -> [5, 9, 6]
transform-> [9, 1, 16, 1, 25, 81, 4, 36]
search 5 -> found 5 at index 4 after 5 comparisons
histogram:
 1 | ##
 2 | #
 3 | #
 4 | #
 5 | #
 6 | #
 9 | #

The program is deterministic, so your numbers will match exactly. expected-output/FIELDS.md lists the required behaviour for every input on every platform.

Validation steps

  1. Run python3 examples/patterns.py demo — it must print the five-pattern report above.
  2. Run echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 7; echo $? — it must print 7 not found after 8 comparisons and then 1.
  3. Complete the five exercises in starter/patterns.py, then run it on the same inputs and confirm it matches the reference.
  4. Run the tests (next section) — every check must pass.

Tests

bash tests/run_tests.sh

Expected final line while the starter is unfinished: 15 checks, 0 failure(s). Once you complete all five starter exercises, ten more checks run your version through the same good/bad inputs plus the main-guard check, giving 24 checks, 0 failure(s). 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 command-line arguments and standard input, and write nothing outside their own console output (no files, no network, no settings). To reset your work, restore the starter from git: git checkout -- starter/patterns.py.

Troubleshooting

See troubleshooting.md for the full list: the program "hanging" while it waits on standard input, python vs python3, the deliberate NotImplementedError stubs, non-number tokens, missing command arguments, histogram alignment, exit codes, and interrupting an infinite loop with Ctrl-C.

Security notes

See security.md. Short version: the program makes no network calls, writes no files, and needs no privileges. Its central habits are to validate input and never eval() it — turn text into numbers with int(), which cannot execute code — and to bound any loop fed by untrusted input so it cannot be pushed into running forever.

Extension exercises

  1. Add a sixth pattern using starter/patterns-worksheet.md: a running maximum and its position, a run-length summary, or a zip-based two-list merge. Give it its own function and subcommand, and validate its input.
  2. Rewrite the filter and transform patterns as one-line list comprehensions ([n for n in nums if n > t], [n*n for n in nums]) and confirm they produce identical output to the loops.
  3. Generate a long input with python3 -c "print(' '.join(str(i) for i in range(10000)))" and pipe it to search for a value near the front and near the end; compare the comparison counts to see what the early break saves.
  • Previous day: Day 50 — Conditionals and Boolean Logic (labs/sections/programming-with-python/day-050-conditionals-and-boolean-logic/).
  • Next day: Day 52 — Lists in Depth (labs/sections/programming-with-python/day-052-lists-in-depth/, to be written).
  • Week 8 project: the collections and control-flow miniproject, which reuses exactly these iteration patterns over richer data structures.

Expected output

FIELDS.md

# Expected output — Day 051 lab

This directory holds real captured runs from the authoring machine
(macOS, Apple Silicon, Python 3.14.0, 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 run on the `demo` command and on
  each pattern with numbers piped in on standard input, plus one bad-input
  case and the `python3 -c` import check.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
  still unfinished (15 checks, 0 failures).

## Required behaviour on every platform

A correct program must, for these inputs, produce exactly:

| Command (stdin in quotes) | Standard output | Exit code |
| ------------------------- | --------------- | --------- |
| `patterns.py demo` (no stdin) | five-pattern report; `total    -> count=8 sum=31` etc. | 0 |
| `"3 1 4 1 5 9 2 6"` → `total` | `count=8 sum=31` | 0 |
| `"3 1 4 1 5 9 2 6"` → `filter 4` | `[5, 9, 6]` | 0 |
| `"3 1 4 1 5 9 2 6"` → `transform` | `[9, 1, 16, 1, 25, 81, 4, 36]` | 0 |
| `"3 1 4 1 5 9 2 6"` → `search 5` | `found 5 at index 4 after 5 comparisons` | 0 |
| `"3 1 4 1 5 9 2 6"` → `search 7` | `7 not found after 8 comparisons` | 1 |
| `"1 2 2 3 3 3"` → `histogram` | `1 | #` / `2 | ##` / `3 | ###` | 0 |
| `"1 x 3"` → `total` | (stderr) `error: 'x' is not a whole number` | 1 |
| `"1 2 3"` → `frobnicate` | (stderr) `error: unknown command 'frobnicate'` | 1 |
| `"1 2 3"` → `filter` (no threshold) | (stderr) `error: filter needs one threshold, ...` | 1 |

Two details make this deterministic and testable:

- **Input comes from standard input (or the built-in `demo` sample), not an
  interactive prompt.** That is why the tests can pipe data with `echo` and
  never hang waiting for a human.
- **The found and not-found search cases exit with different codes (0 vs 1),**
  so another program can tell whether the value was present without parsing
  the text.

The only platform difference is the shell prompt (`$`) shown before each
command; the program's own output is identical everywhere Python 3 runs. On
Windows, run inside WSL, or substitute `python` for `python3` if that is how
Python is exposed.

## Test-suite counts

- With the starter unfinished: `15 checks, 0 failure(s).`
- Once you complete all five starter exercises: `24 checks, 0 failure(s).`
  (the ten extra checks run your finished starter through the same
  good/bad inputs as the reference, plus a check that it has the main guard).

sample-run.txt

$ python3 examples/patterns.py demo
sample: [3, 1, 4, 1, 5, 9, 2, 6]
total    -> count=8 sum=31
filter>4 -> [5, 9, 6]
transform-> [9, 1, 16, 1, 25, 81, 4, 36]
search 5 -> found 5 at index 4 after 5 comparisons
histogram:
1 | ##
2 | #
3 | #
4 | #
5 | #
6 | #
9 | #

$ echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py total
count=8 sum=31

$ echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py filter 4
[5, 9, 6]

$ echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py transform
[9, 1, 16, 1, 25, 81, 4, 36]

$ echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 5   ; echo "exit: $?"
found 5 at index 4 after 5 comparisons
exit: 0

$ echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 7   ; echo "exit: $?"
7 not found after 8 comparisons
exit: 1

$ echo "1 2 2 3 3 3" | python3 examples/patterns.py histogram
1 | #
2 | ##
3 | ###

$ echo "1 x 3" | python3 examples/patterns.py total   ; echo "exit: $?"
error: 'x' is not a whole number
usage: python3 patterns.py <command> [arg]
  commands (read numbers from stdin, except demo):
    demo                 show all five patterns on a sample list
    total                accumulate: print count and sum
    filter <threshold>   keep items greater than threshold
    transform            square each item
    search <target>      find target with an early break
    histogram            counts of each value as '#' bars
exit: 1

$ python3 -c "...from patterns import accumulate_total; print(accumulate_total([3,1,4,1,5,9,2,6]))"
(8, 31)

test-run.txt

Testing <repo>/labs/sections/programming-with-python/day-051-loops-for-while-and-iteration-patterns/examples/patterns.py ...
  ok: demo shows all patterns
  ok: total (accumulate)
  ok: filter > 4
  ok: transform (squares)
  ok: search 5 found early
  ok: search 7 not found
  ok: histogram bars
  ok: non-number rejected
  ok: unknown command rejected
  ok: filter missing arg
Testing importability of examples/patterns.py ...
  ok: import accumulate_total -> (8, 31)
  ok: import linear_search early-exit + not-found
Testing starter/patterns.py ...
  ok: starter is valid Python
Note: starter/patterns.py still has unfinished exercises — testing structure only.
  ok: starter defines accumulate_total
  ok: starter defines build_histogram

15 checks, 0 failure(s).

Source files

examples/patterns.py (6668 bytes)
#!/usr/bin/env python3
"""Iteration Patterns Workbench — a complete, small, real program.

Demonstrates the five core loop patterns on a sequence of whole numbers:

    accumulate  — build a running total and count
    filter      — keep the items that pass a test
    transform   — make a new item from each old one
    search      — scan with an early break, and report the work done
    histogram   — build a small text histogram (counts of each value)

Numbers are read from standard input (whitespace-separated), except the
`demo` command, which uses a built-in sample list so it needs no input.

Usage:
    python3 patterns.py demo                     # show all five patterns
    echo "3 1 4 1 5 9 2 6" | python3 patterns.py total
    echo "3 1 4 1 5 9 2 6" | python3 patterns.py filter 4
    echo "3 1 4 1 5 9 2 6" | python3 patterns.py transform
    echo "3 1 4 1 5 9 2 6" | python3 patterns.py search 5
    echo "1 2 2 3 3 3"     | python3 patterns.py histogram

Bad input (a non-number token, an unknown command, or a missing argument)
prints a clear error to standard error and exits with a non-zero code.
"""
import sys

SAMPLE = [3, 1, 4, 1, 5, 9, 2, 6]


def read_numbers(text):
    """Parse whitespace-separated whole numbers from text into a list of int.

    Raises ValueError naming the first token that is not a whole number.
    """
    numbers = []
    for token in text.split():
        try:
            numbers.append(int(token))
        except ValueError:
            raise ValueError(f"'{token}' is not a whole number")
    return numbers


def accumulate_total(numbers):
    """Accumulator pattern: return (count, total) built one item per pass."""
    count = 0
    total = 0
    for n in numbers:
        count = count + 1
        total = total + n
    return count, total


def filter_above(numbers, threshold):
    """Filter pattern: return the items strictly greater than threshold."""
    kept = []
    for n in numbers:
        if n > threshold:
            kept.append(n)
    return kept


def transform_squares(numbers):
    """Transform pattern: return a new list with each item squared."""
    squares = []
    for n in numbers:
        squares.append(n * n)
    return squares


def linear_search(numbers, target):
    """Search pattern with early break.

    Return (index, comparisons): index is the position of the first item
    equal to target, or -1 if absent; comparisons is how many items were
    inspected — fewer when the target is found early, thanks to break.
    """
    comparisons = 0
    for index, n in enumerate(numbers):
        comparisons = comparisons + 1
        if n == target:
            return index, comparisons
    return -1, comparisons


def build_histogram(numbers):
    """Build a text histogram: for each distinct value, a bar of '#' per count.

    Returns a list of aligned label-and-bar strings, sorted by value.
    """
    counts = {}
    for n in numbers:
        counts[n] = counts.get(n, 0) + 1
    if not counts:
        return []
    width = max(len(str(value)) for value in counts)
    rows = []
    for value in sorted(counts):
        bar = ""
        for _ in range(counts[value]):
            bar = bar + "#"
        rows.append(f"{str(value).rjust(width)} | {bar}")
    return rows


def run_command(command, args, numbers):
    """Dispatch one command. Returns (lines_to_print, exit_code)."""
    if command == "total":
        count, total = accumulate_total(numbers)
        return [f"count={count} sum={total}"], 0

    if command == "filter":
        if len(args) != 1:
            raise ValueError("filter needs one threshold, e.g. filter 4")
        threshold = int_arg(args[0], "threshold")
        return [str(filter_above(numbers, threshold))], 0

    if command == "transform":
        return [str(transform_squares(numbers))], 0

    if command == "search":
        if len(args) != 1:
            raise ValueError("search needs one target, e.g. search 5")
        target = int_arg(args[0], "target")
        index, comparisons = linear_search(numbers, target)
        if index == -1:
            return [f"{target} not found after {comparisons} comparisons"], 1
        return [f"found {target} at index {index} after {comparisons} comparisons"], 0

    if command == "histogram":
        return build_histogram(numbers), 0

    raise ValueError(f"unknown command '{command}'")


def int_arg(text, name):
    """Parse a command argument to int, raising a clear ValueError on failure."""
    try:
        return int(text)
    except ValueError:
        raise ValueError(f"{name} must be a whole number, not '{text}'")


def demo():
    """Run every pattern on the built-in SAMPLE list; needs no input."""
    numbers = SAMPLE
    count, total = accumulate_total(numbers)
    lines = [
        f"sample: {numbers}",
        f"total    -> count={count} sum={total}",
        f"filter>4 -> {filter_above(numbers, 4)}",
        f"transform-> {transform_squares(numbers)}",
    ]
    index, comparisons = linear_search(numbers, 5)
    lines.append(
        f"search 5 -> found 5 at index {index} after {comparisons} comparisons"
    )
    lines.append("histogram:")
    lines.extend(build_histogram(numbers))
    return lines


def main(argv):
    """Entry point. Returns an exit code: 0 on success, non-zero on error."""
    if len(argv) < 2:
        print("error: no command given", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 1

    command = argv[1]
    args = argv[2:]

    if command in ("-h", "--help", "help"):
        print(usage())
        return 0

    if command == "demo":
        for line in demo():
            print(line)
        return 0

    try:
        numbers = read_numbers(sys.stdin.read())
        lines, code = run_command(command, args, numbers)
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 1

    for line in lines:
        print(line)
    return code


def usage():
    """Return the one-block usage string."""
    return (
        "usage: python3 patterns.py <command> [arg]\n"
        "  commands (read numbers from stdin, except demo):\n"
        "    demo                 show all five patterns on a sample list\n"
        "    total                accumulate: print count and sum\n"
        "    filter <threshold>   keep items greater than threshold\n"
        "    transform            square each item\n"
        "    search <target>      find target with an early break\n"
        "    histogram            counts of each value as '#' bars"
    )


if __name__ == "__main__":
    sys.exit(main(sys.argv))
metadata.yml (788 bytes)
lesson_id: D051
day: 51
kind: python-program
languages: [python]
setup_commands:
  - cd labs/sections/programming-with-python/day-051-loops-for-while-and-iteration-patterns
  - python3 --version
run_commands:
  - python3 examples/patterns.py demo
  - echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py total
  - echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py filter 4
  - echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py search 5
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/patterns.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 → 15 checks, 0 failure(s), exit 0'
requirements/README.md (923 bytes)
# Dependencies — Day 051 lab

**Python 3 only. No third-party packages.**

- `python3` (3.8 or newer; tested on 3.14.0). Preinstalled on most Linux
  distributions and installable on macOS; you set this up on Day 43.
- `bash` for the test runner (preinstalled on macOS and Linux).
- Only the Python standard library is used — specifically the `sys` module,
  which ships with Python. There is deliberately no `requirements.txt`: an
  iteration workbench should 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. Because the data commands read from
standard input, pipe input with `echo "..." | python3 ...` rather than typing
it interactively.
starter/patterns-worksheet.md (2033 bytes)
# Iteration pattern worksheet — Day 051

Use this sheet for the **practice assignment**: design a sixth pattern of
your own *before* you code it, then record how it behaved. Designing the loop
on paper first is the habit that prevents infinite loops and off-by-one
errors. Keep this file — the collections you meet on Days 52-54 give you
richer things to iterate over with exactly these patterns.

## 1. What one job does your new pattern do?

One sentence:

> _e.g. Report the largest number and the index where it first appeared._

## 2. Which loop shape, and why?

- [ ] `for` loop — I am visiting each item of a known collection once
- [ ] `while` loop — I am repeating until a condition becomes false

Reason:

> _________________________________________________________________________

## 3. Accumulator design

| Question | Answer |
| -------- | ------ |
| What variable(s) build up the result? |  |
| What neutral value does each start at? (0, "", [], None, ...) |  |
| What happens to it on each pass? |  |
| Do you `break` early? If so, on what condition? |  |

## 4. Inputs and outputs

- Input source (stdin / a fixed sample / argv): ____________________
- On success, it prints: ____________________  (exit code ____)
- On bad input, it prints (to stderr): ____________________  (exit code ____)

## 5. Edge cases — at least two

| Edge case | What the loop should do |
| --------- | ----------------------- |
| empty input (no numbers) |  |
| a single-item input |  |
| _(your choice)_ |  |

## 6. Behaviour on real inputs (fill in after building)

- One NORMAL input you ran: `___________________`
  - What it printed: `___________________`
  - Exit code: ____
- One EDGE-CASE input you ran: `___________________`
  - What it printed: `___________________`
  - Exit code: ____

## 7. Cost check

If your pattern used a nested loop, how many times would the inner body run
on an input of N items? (Use the outer-times-inner rule.)

> _________________________________________________________________________
starter/patterns.py (6894 bytes)
#!/usr/bin/env python3
"""Iteration Patterns Workbench — YOUR working file.

Complete the five numbered exercises below, one per loop pattern. Each stub
raises NotImplementedError until you finish it. The finished reference is in
examples/patterns.py — try each exercise yourself before peeking.

When all five are done, this file behaves just like the reference:

    python3 starter/patterns.py demo
    echo "3 1 4 1 5 9 2 6" | python3 starter/patterns.py total   -> count=8 sum=31

Then run:  bash tests/run_tests.sh
"""
import sys

SAMPLE = [3, 1, 4, 1, 5, 9, 2, 6]


def read_numbers(text):
    """Parse whitespace-separated whole numbers into a list of int.

    Raises ValueError naming the first token that is not a whole number.
    (Provided — study how the loop validates each token at the boundary.)
    """
    numbers = []
    for token in text.split():
        try:
            numbers.append(int(token))
        except ValueError:
            raise ValueError(f"'{token}' is not a whole number")
    return numbers


def accumulate_total(numbers):
    """Accumulator pattern: return (count, total)."""
    # Exercise 1: ACCUMULATE.
    # Start count and total at 0 (the neutral values). Loop over `numbers`
    # with a for loop; each pass, add 1 to count and add the item to total.
    # Return (count, total). Verify by hand: [3,1,4,1,5,9,2,6] -> (8, 31).
    raise NotImplementedError("Exercise 1: implement accumulate_total")


def filter_above(numbers, threshold):
    """Filter pattern: return the items strictly greater than threshold."""
    # Exercise 2: FILTER.
    # Start with an empty list `kept`. Loop over `numbers`; if an item is
    # greater than `threshold`, append it to `kept`. Return `kept`.
    # Example: filter_above([3,1,4,1,5,9,2,6], 4) -> [5, 9, 6].
    raise NotImplementedError("Exercise 2: implement filter_above")


def transform_squares(numbers):
    """Transform pattern: return a new list with each item squared."""
    # Exercise 3: TRANSFORM.
    # Start with an empty list. Loop over `numbers`; append n * n for each n.
    # Example: transform_squares([3,1,4]) -> [9, 1, 16].
    raise NotImplementedError("Exercise 3: implement transform_squares")


def linear_search(numbers, target):
    """Search pattern with early break.

    Return (index, comparisons): index of the first item equal to target,
    or -1 if absent; comparisons is how many items were inspected.
    """
    # Exercise 4: SEARCH WITH EARLY BREAK.
    # Count comparisons starting at 0. Loop with enumerate(numbers); each
    # pass, add 1 to comparisons. As soon as an item equals `target`, RETURN
    # (index, comparisons) immediately (this is the early exit — do not keep
    # scanning). If the loop finishes without a match, return (-1, comparisons).
    raise NotImplementedError("Exercise 4: implement linear_search")


def build_histogram(numbers):
    """Build a text histogram: for each distinct value, a bar of '#' per count."""
    # Exercise 5: HISTOGRAM (accumulate counts, then build bars).
    # 1. Build a dict `counts` mapping each value to how many times it appears
    #    (loop over numbers; counts[n] = counts.get(n, 0) + 1).
    # 2. If counts is empty, return [].
    # 3. Let width = the length of the widest value's text (for alignment).
    # 4. For each value in sorted(counts), build a bar of counts[value] '#'
    #    characters and append f"{str(value).rjust(width)} | {bar}" to a list.
    # Return the list. Example on [1,2,2,3,3,3]: ['1 | #', '2 | ##', '3 | ###'].
    raise NotImplementedError("Exercise 5: implement build_histogram")


# ---------------------------------------------------------------------------
# Everything below is provided: command dispatch, main, and usage. Once the
# five functions above work, the whole program runs like the reference.
# ---------------------------------------------------------------------------


def int_arg(text, name):
    """Parse a command argument to int, raising a clear ValueError on failure."""
    try:
        return int(text)
    except ValueError:
        raise ValueError(f"{name} must be a whole number, not '{text}'")


def run_command(command, args, numbers):
    """Dispatch one command. Returns (lines_to_print, exit_code)."""
    if command == "total":
        count, total = accumulate_total(numbers)
        return [f"count={count} sum={total}"], 0
    if command == "filter":
        if len(args) != 1:
            raise ValueError("filter needs one threshold, e.g. filter 4")
        return [str(filter_above(numbers, int_arg(args[0], "threshold")))], 0
    if command == "transform":
        return [str(transform_squares(numbers))], 0
    if command == "search":
        if len(args) != 1:
            raise ValueError("search needs one target, e.g. search 5")
        index, comparisons = linear_search(numbers, int_arg(args[0], "target"))
        if index == -1:
            return [f"{args[0]} not found after {comparisons} comparisons"], 1
        return [f"found {args[0]} at index {index} after {comparisons} comparisons"], 0
    if command == "histogram":
        return build_histogram(numbers), 0
    raise ValueError(f"unknown command '{command}'")


def demo():
    """Run every pattern on the built-in SAMPLE list; needs no input."""
    numbers = SAMPLE
    count, total = accumulate_total(numbers)
    lines = [
        f"sample: {numbers}",
        f"total    -> count={count} sum={total}",
        f"filter>4 -> {filter_above(numbers, 4)}",
        f"transform-> {transform_squares(numbers)}",
    ]
    index, comparisons = linear_search(numbers, 5)
    lines.append(f"search 5 -> found 5 at index {index} after {comparisons} comparisons")
    lines.append("histogram:")
    lines.extend(build_histogram(numbers))
    return lines


def usage():
    """Return the one-block usage string."""
    return (
        "usage: python3 patterns.py <command> [arg]\n"
        "  commands (read numbers from stdin, except demo):\n"
        "    demo, total, filter <threshold>, transform, search <target>, histogram"
    )


def main(argv):
    """Entry point. Returns an exit code: 0 on success, non-zero on error."""
    if len(argv) < 2:
        print("error: no command given", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 1
    command, args = argv[1], argv[2:]
    if command in ("-h", "--help", "help"):
        print(usage())
        return 0
    if command == "demo":
        for line in demo():
            print(line)
        return 0
    try:
        numbers = read_numbers(sys.stdin.read())
        lines, code = run_command(command, args, numbers)
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print(usage(), file=sys.stderr)
        return 1
    for line in lines:
        print(line)
    return code


if __name__ == "__main__":
    sys.exit(main(sys.argv))
tests/run_tests.sh (4586 bytes)
#!/usr/bin/env bash
# Tests for the Day 051 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Exercises the complete reference program (examples/patterns.py) on the five
# iteration patterns, feeding numbers on standard input and checking both the
# printed output and the process exit code. It then imports two pattern
# functions and checks their return values, and finally checks 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

export PYTHONDONTWRITEBYTECODE=1

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/patterns.py"
starter="${lab_dir}/starter/patterns.py"
data="3 1 4 1 5 9 2 6"
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
}

# check_stdin <label> <script> <input> <expect_exit> <needle> <cmd> [args...]
# Pipes <input> to the program's stdin, checks its exit code and that its
# combined output contains <needle>.
check_stdin() {
  local label="$1" script="$2" input="$3" expect_exit="$4" needle="$5"
  shift 5
  local out code
  out="$(printf '%s' "${input}" | python3 "${script}" "$@" 2>&1)"
  code=$?
  if [ "${code}" -eq "${expect_exit}" ] && printf '%s' "${out}" | grep -qF "${needle}"; then
    check "${label}" "yes"
  else
    check "${label}" "no"
    echo "    (exit ${code}, expected ${expect_exit}; output: ${out})"
  fi
}

run_program_checks() {
  local script="$1"
  echo "Testing ${script} ..."
  # demo needs no stdin; the rest read numbers from stdin.
  check_stdin "demo shows all patterns" "${script}" ""      0 "count=8 sum=31"          demo
  check_stdin "total (accumulate)"      "${script}" "${data}" 0 "count=8 sum=31"         total
  check_stdin "filter > 4"              "${script}" "${data}" 0 "[5, 9, 6]"              filter 4
  check_stdin "transform (squares)"     "${script}" "${data}" 0 "[9, 1, 16, 1, 25, 81, 4, 36]" transform
  check_stdin "search 5 found early"    "${script}" "${data}" 0 "found 5 at index 4 after 5 comparisons" search 5
  check_stdin "search 7 not found"      "${script}" "${data}" 1 "7 not found after 8 comparisons" search 7
  check_stdin "histogram bars"          "${script}" "1 2 2 3 3 3" 0 "3 | ###"           histogram
  # Bad input is validated at the boundary.
  check_stdin "non-number rejected"     "${script}" "1 x 3"   1 "is not a whole number"  total
  check_stdin "unknown command rejected" "${script}" "1 2 3"  1 "unknown command"        frobnicate
  check_stdin "filter missing arg"      "${script}" "1 2 3"   1 "filter needs one threshold" filter
}

# --- Reference program: always tested strictly ---
run_program_checks "${ref}"

# --- Import functions and check return values (module is importable) ---
echo "Testing importability of examples/patterns.py ..."
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from patterns import accumulate_total; \
assert accumulate_total([3, 1, 4, 1, 5, 9, 2, 6]) == (8, 31)"; then
  check "import accumulate_total -> (8, 31)" "yes"
else
  check "import accumulate_total -> (8, 31)" "no"
fi
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from patterns import linear_search; \
assert linear_search([3, 1, 4, 1, 5], 5) == (4, 5); \
assert linear_search([1, 2, 3], 9) == (-1, 3)"; then
  check "import linear_search early-exit + not-found" "yes"
else
  check "import linear_search early-exit + not-found" "no"
fi

# --- Learner starter ---
echo "Testing starter/patterns.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/patterns.py still has unfinished exercises — testing structure only."
  grep -q 'def accumulate_total' "${starter}" && check "starter defines accumulate_total" "yes" || check "starter defines accumulate_total" "no"
  grep -q 'def build_histogram' "${starter}" && check "starter defines build_histogram" "yes" || check "starter defines build_histogram" "no"
else
  run_program_checks "${starter}"
  grep -q '__name__ == "__main__"' "${starter}" && check "starter has the main guard" "yes" || check "starter has the main guard" "no"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 051 lab

The program seems to hang with no output

A data command (total, filter, transform, search, histogram) reads its numbers from standard input. If you run it with nothing piped in, it waits for you to type input. Either pipe data:

echo "3 1 4 1 5 9 2 6" | python3 examples/patterns.py total

or, if you started it bare, type numbers and press Ctrl-D to signal end-of-input. The demo command needs no input — it uses a built-in sample.

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 five exercises. Each unfinished pattern function raises NotImplementedError on purpose so you cannot mistake an empty function for a working one. Replace each raise NotImplementedError(...) line with the loop described in the comment above it. Once all five are done, the file runs exactly like examples/patterns.py.

... is not a whole number

The program validates input at the boundary: every token on standard input must be an integer. A stray letter or punctuation mark (for example 1 x 3) is reported by name and the program exits non-zero, instead of crashing with a raw traceback. Check your input for non-numeric tokens.

filter needs one threshold / search needs one target

Those two commands take one number after the command name: filter 4, search 5. Running them bare is rejected with a usage message. The number is a command argument (read from argv), separate from the data on standard input.

My histogram bars look misaligned

Each value's label is right-aligned to the width of the widest value so the | separators line up. Build each bar as count copies of #, and pad the label (with .rjust(width)), not the bar. Verify on 1 2 2 3 3 3, which must give 1 | #, 2 | ##, 3 | ###.

The found and not-found search cases must end with different exit codes (0 for found, non-zero for not found) so a caller can tell them apart. If both return 0, re-check that the not-found path returns exit code 1.

An infinite loop froze my terminal

If you experiment with a while loop and it never ends, press Ctrl-C to interrupt it. The cause is almost always that nothing in the body makes the condition move toward false — add the missing progress step (a counter increment, or a reachable break).

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.

Security notes

Security notes — Day 051 lab

  • What the program does: reads a command name and optional argument from argv, reads whitespace-separated numbers from standard input, runs one iteration pattern, and prints the result. It makes no network connections, reads and writes no files, and changes no settings. The test runner is equally self-contained and non-interactive.

  • Validate input; never execute it. The program turns input text into numbers with int(), which can only ever produce an integer or raise an error — it cannot run code. Never use eval() or exec() on input, no matter how convenient it seems for "parsing" what a user typed: those functions execute the string as Python, so a malicious value could delete files or open a network connection. Parse and validate input into safe types at the boundary, exactly as read_numbers and int_arg do.

  • Bound loops fed by external input. A loop whose length is controlled by untrusted input is a denial-of-service lever: an attacker who can make your loop run "one more time" a billion times can freeze the program. This lab's loops are bounded by the input actually provided, and a real service should additionally cap how much input it will accept. The general rule: never trust a stopping condition that depends entirely on data you did not generate, and give loops over external input a hard upper bound.

  • Fail loudly, not silently. On bad input the program prints a clear message to standard error and exits with a non-zero code. Silent failure — computing a wrong answer from bad input and reporting it confidently — is worse than a crash, because no one notices. Validating at the boundary prevents both.

  • 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/patterns.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.