Programming with PythonControl Flow and Collections › Day 50

Hands-on lab — Day 50: Conditionals and Boolean Logic

Commands

Setup

cd labs/sections/programming-with-python/day-050-conditionals-and-boolean-logic
python3 --version

Run

python3 examples/triage.py 0.95 verified
python3 examples/triage.py 0.95 unverified
python3 starter/triage.py 0.70 verified

Test

bash tests/run_tests.sh

File tree

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

Lab README

Day 050 lab — Build a Decision Engine

Lesson

Purpose

Day 50's lesson teaches how a program decides: booleans, comparison and logical operators, short-circuit evaluation, if/elif/else, the ternary, and guard clauses. This lab makes that concrete: you build a decision engine, triage.py, that classifies a model prediction into AUTO_ACCEPT, REVIEW, or REJECT from a confidence score and a verification status. It reads input from the command line, validates it with a chained comparison, rejects low confidence with a guard clause, admits confident-and-verified predictions with a short-circuiting and, labels the confidence band with a nested ternary, and fails gracefully on bad input. You build it from a starter, one exercise at a time, then run an automated test suite that checks real behaviour. This is the routing shape that sits in front of real model-serving endpoints.

Learning objectives

  • Read and understand a complete, well-structured decision program.
  • Produce booleans with comparison operators and a chained comparison (0.0 <= score <= 1.0).
  • Combine booleans with and and rely on short-circuit evaluation.
  • Reject bad input early with a guard clause, and choose values with a nested conditional (ternary) expression.
  • Validate input at the boundary and report bad input with a clear message and a non-zero exit code.

Prerequisites

  • The Day 50 lesson (read it first — it explains every part this lab builds).
  • Days 43-49: a working Python 3 install plus variables, strings, numbers, input/output, error messages, and assembling a small program with functions and a main() guard.
  • 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).
  • 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 comparisons and arithmetic on two values; 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 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. The lesson's optional linters (Ruff, Pylint, flake8), type checker (mypy), and test framework (pytest) are all free and open source too.

Installation

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

cd labs/sections/programming-with-python/day-050-conditionals-and-boolean-logic
python3 --version   # confirm Python 3.8+ is available

File structure

day-050-conditionals-and-boolean-logic/
├── README.md                       ← you are here
├── metadata.yml                    ← machine-readable lab metadata
├── starter/
│   ├── triage.py                   ← YOUR working file (5 numbered exercises)
│   └── decision-worksheet.md       ← design a second engine before coding it
├── examples/
│   └── triage.py                   ← complete reference implementation
├── tests/
│   └── run_tests.sh                ← automated checks (good + bad inputs, 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 the finished decision engine on good and bad input
python3 examples/triage.py 0.95 verified
python3 examples/triage.py 0.95 unverified
python3 examples/triage.py 0.30 verified
python3 examples/triage.py 1.5 verified       # invalid: prints an error, exits non-zero

## 2. Your task: complete the five exercises in the starter, then run it
python3 starter/triage.py 0.70 verified

## 3. Prove the module is importable (the payoff of the main guard)
python3 -c "import sys; sys.path.insert(0, 'examples'); from triage import classify; print(classify(0.95, True))"

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

What the commands do

  • python3 examples/triage.py 0.95 verified — runs the complete reference program: it reads the score and status from the command line, validates them in parse_args, decides in classify, labels the band in confidence_band, formats with format_result, and prints score=0.95 verified=True -> AUTO_ACCEPT (confidence: high). Bad input (a non-number, a score outside 0.0–1.0, an unknown status, a missing argument) prints a clear error to standard error and exits with code 2.
  • python3 starter/triage.py 0.70 verified — runs your version. The starter ships with five exercises stubbed out (each raising NotImplementedError until you finish it): write the ternary band, write the classification ladder, validate input, format output, and add the main guard.
  • python3 -c "...classify..." — 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 nine 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/triage.py 0.95 verified
score=0.95 verified=True  -> AUTO_ACCEPT (confidence: high)

$ python3 examples/triage.py 1.5 verified   ; echo "exit: $?"
error: score 1.5 is out of range (expected 0.0 to 1.0)
usage: python3 triage.py <score> <status>   (score 0.0-1.0, status verified|unverified)
exit: 2

The decision prints to standard output; the error prints to standard error and sets exit code 2. 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/triage.py 0.95 verified — it must print AUTO_ACCEPT.
  2. Run python3 examples/triage.py 1.5 verified; echo $? — it must print an error and then 2.
  3. Complete the five exercises in starter/triage.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: 14 checks, 0 failure(s). Once you complete all five starter exercises, more checks run your version through the same good/bad inputs plus the main-guard check, giving 22 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 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/triage.py.

Troubleshooting

See troubleshooting.md for the full list: python vs python3, the deliberate NotImplementedError stubs, = vs ==, branch ordering, boundary comparisons, and exit-code checks.

Security notes

See security.md. Short version: the program makes no network calls, writes no files, and needs no privileges. Its central lesson is to validate input at the boundary and never eval() it — turn text into numbers with float(), which cannot execute code.

Extension exercises

  1. Add a De Morgan simplification: write a condition both ways in a comment (not (verified and score >= 0.9) and not verified or score < 0.9) and add a test that runs the function over a grid of scores and flags and asserts the two forms always agree.
  2. Add a new outcome, ESCALATE, for a very high score that is still unverified, and update both classify and tests/run_tests.sh to cover it.
  3. Write your own tests/test_triage.py that imports the functions and asserts several known results with assert, printing all tests passed only if every assertion holds; run it with python3 tests/test_triage.py.
  • Previous day: Day 49 — Your First Real Program (labs/sections/programming-with-python/day-049-your-first-real-program/).
  • Next day: Day 51 — continues Week 8, Control Flow and Collections (labs/sections/programming-with-python/day-051-.../, to be written).
  • Week 8 project: a rule-driven classifier that extends exactly this shape — parse input, validate, decide with clear boolean logic, print clearly, fail gracefully.

Expected output

FIELDS.md

# Expected output — Day 050 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 decision engine is deterministic — the same input always
produces the same output on every platform.

## Files

- `sample-run.txt` — the reference program run on good and bad inputs, plus the
  `python3 -c` import check.
- `test-run.txt` — a full run of `bash tests/run_tests.sh` with the starter
  still unfinished (14 checks, 0 failures).

## Required behaviour on every platform

A correct decision engine must, for these inputs, produce exactly:

| Command | Standard output | Exit code |
| ------- | --------------- | --------- |
| `triage.py 0.95 verified` | `score=0.95 verified=True  -> AUTO_ACCEPT (confidence: high)` | 0 |
| `triage.py 0.95 unverified` | `score=0.95 verified=False -> REVIEW (confidence: high)` | 0 |
| `triage.py 0.30 verified` | `score=0.30 verified=True  -> REJECT (confidence: low)` | 0 |
| `triage.py 0.90 verified` | `score=0.90 verified=True  -> AUTO_ACCEPT (confidence: high)` | 0 |
| `triage.py 0.50 verified` | `score=0.50 verified=True  -> REVIEW (confidence: medium)` | 0 |
| `triage.py hot verified` | (stderr) `error: 'hot' is not a number` | 2 |
| `triage.py 1.5 verified` | (stderr) `error: score 1.5 is out of range ...` | 2 |
| `triage.py 0.8 maybe` | (stderr) `error: status must be verified or unverified ...` | 2 |
| `triage.py 0.8` | (stderr) `error: expected 2 arguments: ...` | 2 |

The classification prints to standard output; errors print to standard error
and set exit code 2. The only platform difference is the shell prompt shown
before each command (`$` here); the program's own output is identical
everywhere Python 3 runs. On Windows, substitute `python` for `python3` if that
is how Python is exposed, or run inside WSL.

## Test-suite counts

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

sample-run.txt

$ python3 examples/triage.py 0.95 verified
score=0.95 verified=True  -> AUTO_ACCEPT (confidence: high)

$ python3 examples/triage.py 0.95 unverified
score=0.95 verified=False -> REVIEW (confidence: high)

$ python3 examples/triage.py 0.30 verified
score=0.30 verified=True  -> REJECT (confidence: low)

$ python3 examples/triage.py 0.90 verified
score=0.90 verified=True  -> AUTO_ACCEPT (confidence: high)

$ python3 examples/triage.py 0.50 verified
score=0.50 verified=True  -> REVIEW (confidence: medium)

$ python3 examples/triage.py 1.5 verified   ; echo "exit: $?"
error: score 1.5 is out of range (expected 0.0 to 1.0)
usage: python3 triage.py <score> <status>   (score 0.0-1.0, status verified|unverified)
exit: 2

$ python3 examples/triage.py hot verified   ; echo "exit: $?"
error: 'hot' is not a number
usage: python3 triage.py <score> <status>   (score 0.0-1.0, status verified|unverified)
exit: 2

$ python3 examples/triage.py 0.8 maybe      ; echo "exit: $?"
error: status must be verified or unverified, not 'maybe'
usage: python3 triage.py <score> <status>   (score 0.0-1.0, status verified|unverified)
exit: 2

$ python3 examples/triage.py 0.8            ; echo "exit: $?"
error: expected 2 arguments: <score> <status> (e.g. 0.95 verified)
usage: python3 triage.py <score> <status>   (score 0.0-1.0, status verified|unverified)
exit: 2

$ python3 -c "import sys; sys.path.insert(0,'examples'); from triage import classify, confidence_band; print(classify(0.95, True), confidence_band(0.5))"
AUTO_ACCEPT medium

test-run.txt

Testing <repo>/labs/sections/programming-with-python/day-050-conditionals-and-boolean-logic/examples/triage.py ...
  ok: 0.95 verified -> AUTO_ACCEPT
  ok: 0.95 unverified -> REVIEW
  ok: 0.30 verified -> REJECT
  ok: 0.90 boundary -> AUTO_ACCEPT
  ok: 0.50 boundary -> REVIEW
  ok: non-number score rejected
  ok: out-of-range score rejected
  ok: bad status rejected
  ok: wrong arg count rejected
Testing importability of examples/triage.py ...
  ok: import classify() returns correct decisions
  ok: import confidence_band() returns correct bands
Testing starter/triage.py ...
  ok: starter is valid Python
Note: starter/triage.py still has unfinished exercises — testing structure only.
  ok: starter defines classify
  ok: starter defines confidence_band

14 checks, 0 failure(s).

Source files

examples/triage.py (3575 bytes)
#!/usr/bin/env python3
"""Decision engine: classify a model prediction into ACCEPT / REVIEW / REJECT.

This is a complete, small, real program that shows every idea from the Day 50
lesson working together: comparison operators, the logical `and` with
short-circuit evaluation, an `if`/`elif`/`else`-style ladder, a guard clause,
a chained comparison for range validation, and a (nested) conditional
expression. It reads its input from the command line, validates it, does one
useful job, prints clear output, and fails gracefully on bad input.

Usage:
    python3 triage.py <score> <status>

<score>  is the model's confidence, a number from 0.0 to 1.0.
<status> is whether an upstream check passed: verified or unverified.

Examples:
    python3 triage.py 0.95 verified     ->  AUTO_ACCEPT
    python3 triage.py 0.95 unverified   ->  REVIEW
    python3 triage.py 0.30 verified     ->  REJECT
"""
import sys

VALID_STATUSES = ("verified", "unverified")


def confidence_band(score):
    """Return 'high', 'medium', or 'low' for a score in 0.0-1.0.

    A three-way choice written as a nested conditional (ternary) expression.
    """
    return "high" if score >= 0.9 else ("medium" if score >= 0.5 else "low")


def classify(score, verified):
    """Return the routing decision for a prediction.

    Uses a guard clause to reject low confidence first, then the logical
    `and` (which short-circuits) to auto-accept only what is both confident
    and verified, and falls through to REVIEW for everything else.
    """
    if score < 0.5:                       # guard clause: turn away low confidence
        return "REJECT"
    if score >= 0.9 and verified:         # confident AND checked: safe to accept
        return "AUTO_ACCEPT"
    return "REVIEW"                        # medium confidence, or high-but-unverified


def parse_args(args):
    """Validate raw [score, status] arguments and return (float, bool).

    Raises ValueError with a human-readable message on any bad input:
    wrong argument count, a non-numeric score, an unknown status, or a
    score outside the 0.0-1.0 range.
    """
    if len(args) != 2:
        raise ValueError("expected 2 arguments: <score> <status> (e.g. 0.95 verified)")
    score_text, status_text = args
    try:
        score = float(score_text)
    except ValueError:
        raise ValueError(f"'{score_text}' is not a number")
    status = status_text.strip().lower()
    if status not in VALID_STATUSES:
        raise ValueError(f"status must be verified or unverified, not '{status_text}'")
    if not 0.0 <= score <= 1.0:           # chained comparison: range check
        raise ValueError(f"score {score} is out of range (expected 0.0 to 1.0)")
    verified = status == "verified"
    return score, verified


def format_result(score, verified, category, band):
    """Return the one-line, human-readable decision string."""
    return f"score={score:.2f} verified={str(verified):<5} -> {category} (confidence: {band})"


def main(argv):
    """Program entry point. Returns an exit code: 0 on success, 2 on bad input."""
    try:
        score, verified = parse_args(argv[1:])
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print("usage: python3 triage.py <score> <status>   "
              "(score 0.0-1.0, status verified|unverified)", file=sys.stderr)
        return 2
    category = classify(score, verified)
    band = confidence_band(score)
    print(format_result(score, verified, category, band))
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
metadata.yml (678 bytes)
lesson_id: D050
day: 50
kind: python-program
languages: [python]
setup_commands:
  - cd labs/sections/programming-with-python/day-050-conditionals-and-boolean-logic
  - python3 --version
run_commands:
  - python3 examples/triage.py 0.95 verified
  - python3 examples/triage.py 0.95 unverified
  - python3 starter/triage.py 0.70 verified
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/triage.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 → 14 checks, 0 failure(s), exit 0'
requirements/README.md (1001 bytes)
# Dependencies — Day 050 lab

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

- `python3` (3.8 or newer; tested on 3.14). 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`: a
  decision engine this small 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.

The lesson mentions optional free tools you may install later to keep boolean
logic honest — `ruff`, `pylint`, `flake8`, `mypy`, and `pytest` — but none of
them are required to build or test this lab.
starter/decision-worksheet.md (1756 bytes)
# Decision-engine design worksheet

Design your program *before* you code it. Fill in every section for a decision
engine of your own choosing (spam-or-not filter, loan pre-check, support-ticket
priority router, temperature alert — anything with clear rules). Then implement
it with the same shape as `triage.py`.

## 1. The job (one sentence)

> Example: Route a model prediction to AUTO_ACCEPT, REVIEW, or REJECT based on
> its confidence score and whether an upstream check passed.

_Your job:_

## 2. Inputs (what arrives, and from where)

List each input, its type, and where it comes from (command-line argument,
file, etc.).

| Input | Type | Source | Example |
| ----- | ---- | ------ | ------- |
| _e.g. score_ | _float 0.0-1.0_ | _sys.argv[1]_ | _0.95_ |
|  |  |  |  |

## 3. Rules (each as a boolean condition)

Write every rule as a condition, using `and` / `or` / `not`. Mark which one is
a **guard clause** (checked first, exits early) and note any **chained
comparison** (e.g. `0.0 <= score <= 1.0`).

- Guard clause: _________________________________________________
- Rule 1: _______________________________________________________
- Rule 2: _______________________________________________________
- Rule 3: _______________________________________________________

## 4. Outcomes (categories, and which rule leads to each)

| Outcome | Reached when | Exit code |
| ------- | ------------ | --------- |
| _e.g. AUTO_ACCEPT_ | _score >= 0.9 and verified_ | _0_ |
|  |  |  |

## 5. Edge cases you will reject

List at least three inputs your program must refuse, and the message it prints.

1.
2.
3.

## 6. Where a ternary fits

Name one place a conditional (ternary) expression makes a clean two-way (or
nested three-way) value choice.

>
starter/triage.py (3593 bytes)
#!/usr/bin/env python3
"""Decision engine — YOUR working file.

Build this program one exercise at a time. Each numbered exercise below names
exactly what to write, using the boolean logic from the Day 50 lesson. The
finished reference is in examples/triage.py — try each exercise yourself
before peeking.

When you have completed all five exercises, this file should behave just like
the reference:

    python3 starter/triage.py 0.95 verified   ->  ... AUTO_ACCEPT (confidence: high)
    python3 starter/triage.py 1.5 verified    ->  error (exit code 2)

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

VALID_STATUSES = ("verified", "unverified")


def confidence_band(score):
    """Return 'high', 'medium', or 'low' for a score in 0.0-1.0."""
    # Exercise 1: WRITE A NESTED TERNARY (conditional expression).
    # Return "high" when score >= 0.9, "medium" when score >= 0.5,
    # otherwise "low". Do it in one line:
    #   return "high" if score >= 0.9 else ("medium" if score >= 0.5 else "low")
    # Verify by hand: confidence_band(0.5) must be "medium".
    raise NotImplementedError("Exercise 1: implement confidence_band")


def classify(score, verified):
    """Return the routing decision: REJECT, AUTO_ACCEPT, or REVIEW."""
    # Exercise 2: WRITE THE CLASSIFICATION LADDER.
    # 1. Guard clause: if score < 0.5, return "REJECT" immediately.
    # 2. If score >= 0.9 AND verified, return "AUTO_ACCEPT"
    #    (note the logical `and` short-circuits: verified is only checked
    #     when score >= 0.9 is already True).
    # 3. Otherwise return "REVIEW".
    raise NotImplementedError("Exercise 2: implement classify")


def parse_args(args):
    """Validate raw [score, status] arguments and return (float, bool)."""
    # Exercise 3: VALIDATE INPUT (guard the boundary).
    # 1. If len(args) != 2, raise ValueError with a clear message.
    # 2. Convert args[0] to float inside try/except; on failure raise
    #    ValueError(f"'{args[0]}' is not a number").
    # 3. Normalise args[1] with .strip().lower(); if it is not in
    #    VALID_STATUSES, raise ValueError naming the allowed statuses.
    # 4. Use a CHAINED COMPARISON to reject an out-of-range score:
    #    if not 0.0 <= score <= 1.0: raise ValueError(...).
    # 5. Return (score, status == "verified").
    raise NotImplementedError("Exercise 3: implement parse_args")


def format_result(score, verified, category, band):
    """Return the one-line, human-readable decision string."""
    # Exercise 4: FORMAT OUTPUT.
    # Return an f-string like:
    #   score=0.95 verified=True  -> AUTO_ACCEPT (confidence: high)
    # Show the score to two decimals (:.2f) and left-pad the verified flag
    # to width 5 so True and False line up: {str(verified):<5}.
    raise NotImplementedError("Exercise 4: implement format_result")


def main(argv):
    """Program entry point. Returns an exit code: 0 on success, 2 on bad input."""
    try:
        score, verified = parse_args(argv[1:])
    except ValueError as err:
        print(f"error: {err}", file=sys.stderr)
        print("usage: python3 triage.py <score> <status>   "
              "(score 0.0-1.0, status verified|unverified)", file=sys.stderr)
        return 2
    category = classify(score, verified)
    band = confidence_band(score)
    print(format_result(score, verified, category, band))
    return 0


# Exercise 5: ADD THE MAIN GUARD.
# Below this comment, add the guard so the program runs only when this file
# is executed directly (not when it is imported):
#
#     if __name__ == "__main__":
#         sys.exit(main(sys.argv))
tests/run_tests.sh (5273 bytes)
#!/usr/bin/env bash
# Tests for the Day 050 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Exercises the complete reference program (examples/triage.py) on known good
# and bad inputs, checking both the printed output and the process exit code,
# then imports two functions and checks their return values (the payoff of the
# main guard). Finally it checks the learner's starter: structurally while the
# exercises are unfinished, and to the same strict standard once 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)"

# Bytecode left by an EARLIER command is not this run's litter. The README
# documents `pytest starter -q`, and running it writes .pyc files that would
# then fail the cleanliness check at the end of this script -- failing the
# reader for following the instructions. Clearing them here makes that final
# check measure what it claims to: what THIS run left behind. `.venv` is
# untouched, because the packages' own bytecode is theirs, not ours.
find "${lab_dir}" -name '.venv' -prune -o -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
find "${lab_dir}" -name '.venv' -prune -o -type d -name '.pytest_cache' -exec rm -rf {} + 2>/dev/null || true

ref="${lab_dir}/examples/triage.py"
starter="${lab_dir}/starter/triage.py"
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_run <label> <script> <expect_exit> <needle> <arg...>
# Runs the program, checks its exit code equals expect_exit and that its
# combined output contains needle.
check_run() {
  local label="$1" script="$2" expect_exit="$3" needle="$4"
  shift 4
  local out code
  out="$(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} ..."
  # Good inputs: correct decision, exit 0.
  check_run "0.95 verified -> AUTO_ACCEPT" "${script}" 0 "AUTO_ACCEPT (confidence: high)" 0.95 verified
  check_run "0.95 unverified -> REVIEW"    "${script}" 0 "REVIEW (confidence: high)"      0.95 unverified
  check_run "0.30 verified -> REJECT"      "${script}" 0 "REJECT (confidence: low)"       0.30 verified
  check_run "0.90 boundary -> AUTO_ACCEPT" "${script}" 0 "AUTO_ACCEPT"                     0.90 verified
  check_run "0.50 boundary -> REVIEW"      "${script}" 0 "REVIEW (confidence: medium)"    0.50 verified
  # Bad inputs: clear error, exit code 2.
  check_run "non-number score rejected"    "${script}" 2 "is not a number"                hot verified
  check_run "out-of-range score rejected"  "${script}" 2 "out of range"                   1.5 verified
  check_run "bad status rejected"          "${script}" 2 "status must be verified or unverified" 0.8 maybe
  check_run "wrong arg count rejected"     "${script}" 2 "expected 2 arguments"            0.8
}

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

# --- Import functions and check their return values (main-guard payoff) ---
echo "Testing importability of examples/triage.py ..."
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from triage import classify; \
assert classify(0.95, True) == 'AUTO_ACCEPT'; \
assert classify(0.95, False) == 'REVIEW'; \
assert classify(0.30, True) == 'REJECT'"; then
  check "import classify() returns correct decisions" "yes"
else
  check "import classify() returns correct decisions" "no"
fi
if python3 -c "import sys; sys.path.insert(0, '${lab_dir}/examples'); \
from triage import confidence_band; \
assert confidence_band(0.95) == 'high'; \
assert confidence_band(0.50) == 'medium'; \
assert confidence_band(0.10) == 'low'"; then
  check "import confidence_band() returns correct bands" "yes"
else
  check "import confidence_band() returns correct bands" "no"
fi

# --- Learner starter ---
echo "Testing starter/triage.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/triage.py still has unfinished exercises — testing structure only."
  grep -q 'def classify' "${starter}" && check "starter defines classify" "yes" || check "starter defines classify" "no"
  grep -q 'def confidence_band' "${starter}" && check "starter defines confidence_band" "yes" || check "starter defines confidence_band" "no"
else
  # Learner finished: hold the starter to the same strict standard.
  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 050 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 runs like the reference.

SyntaxError pointing at an if line — = versus ==

If you wrote if status = "verified": Python raises a SyntaxError, because = assigns and cannot appear in a condition. Use == to compare: if status == "verified":. This is one of the few beginner bugs Python turns into a hard error rather than a silent one — be grateful, and fix the operator.

A boundary case gives the wrong category

Check whether you meant < or <=. In this engine score >= 0.9 accepts exactly 0.9, and score < 0.5 rejects everything below 0.5 but keeps 0.5 itself. Trace the boundary values 0.0, 0.5, 0.9, and 1.0 by hand and compare with the table in expected-output/FIELDS.md.

The high-confidence branch never runs

You probably tested the broader condition first. In an if/elif ladder the first true branch wins, so if score >= 0.5 comes before score >= 0.9, the >= 0.9 branch is unreachable. Put the most specific (or most exceptional) test first. In classify, the guard score < 0.5 is checked first on purpose.

ModuleNotFoundError: No module named 'triage'

Python imports a module by searching sys.path, which does not include the examples/ subfolder by default. The import one-liners in this lab add it first:

python3 -c "import sys; sys.path.insert(0, 'examples'); from triage import classify; print(classify(0.95, True))"

Run this from the lab directory (the folder that contains examples/), not from inside examples/ itself.

Importing the file runs the whole program

This is exactly the problem the main guard prevents. If importing your module prints output or exits, you either forgot if __name__ == "__main__": (Exercise 5) or wrote program-running code at the top level instead of inside main. Only the guarded sys.exit(main(sys.argv)) should trigger execution.

echo $? shows 0 after a bad input

Your main is not returning 2 on the error path, or you are not passing its return value to sys.exit(). The guard must read sys.exit(main(sys.argv)), and main must return 2 after printing an error. The exit code is how other programs detect that yours refused the input.

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 050 lab

  • What the program does: reads two command-line arguments, decides a category, 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 at the boundary; never execute it. The most important security habit in this lab is turning text into data safely. parse_args checks the argument count, converts the score with float() (which can only ever produce a number or raise an error — it cannot run code), normalises and checks the status against an explicit allow-list, and rejects out-of-range scores with a chained comparison. Never use eval() or exec() on input, no matter how convenient it looks: those functions execute the string as Python, so a malicious value could delete files or open a network connection.

  • Conditionals are your access control. A decision written with or where it needed and can admit input it should have refused. Order matters too: short-circuit evaluation lets you check "is this present and well-formed?" before you act on a value, so a bad input never reaches the code that trusts it. Getting the boolean logic right is the security work here.

  • Fail loudly, not silently. On bad input the program prints a clear message to standard error and exits with a non-zero code (2). Silently classifying a malformed input and reporting a confident wrong answer 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. 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/triage.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.