Programming with Python › Functions and Program Design › Day 61
Hands-on lab — Day 61: Writing Readable Code
- ← Back to the Day 61 lesson
- Open the hands-on files on GitHub — clone or download them from the public labs repository
- Local path in your clone:
labs/sections/programming-with-python/day-061-writing-readable-code/
Commands
Setup
cd labs/sections/programming-with-python/day-061-writing-readable-code
python3 --version Run
python3 starter/messy.py 70 85 90 55 60
python3 examples/report.py 70 85 90 55 60
diff <(python3 starter/messy.py 70 85 90 55 60) <(python3 examples/report.py 70 85 90 55 60) Test
bash tests/run_tests.sh File tree
examples/report.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/messy.py starter/refactor-worksheet.md tests/run_tests.sh troubleshooting.md
Lab README
Day 061 lab — Refactor for Readability
Lesson
- Lesson title: Writing Readable Code
- Day number: 61 of 365
- Lesson article: https://ai-roadmap-365.github.io/day-061-writing-readable-code
- Lab files: everything you need is in this directory — follow “How to run” below.
- Browse the course locally: from the repository root, this lab also appears in the course website at
/labs/day-061-writing-readable-codewhen the site is running.
Purpose
Day 61's lesson is about code that humans — and your future self — can read.
This lab makes it concrete. You start from starter/messy.py: a small program
that works (it prints a summary of the numbers you give it) but is painful
to read — one giant function called d, single-letter variables, no
docstrings, no type hints, a bare magic number, cramped formatting. Your job is
to refactor it into clean, documented, type-hinted, PEP 8 code without
changing what it does — proven by a test suite that passes before and after.
The finished examples/report.py is the target to compare against. This is the
exact discipline that keeps machine-learning experiments reviewable and
reproducible, and it is what AI coding assistants read best.
Learning objectives
- Refactor a working script in small, safe steps, running tests after each so behaviour never changes.
- Replace poor names with meaningful, searchable ones and a magic number with a named constant.
- Decompose one giant function into small, single-purpose functions.
- Add docstrings that say why, and type hints that document and help tools.
- Run an optional formatter (Black) and linter (Ruff/flake8), and understand that a missing optional tool must degrade gracefully, never hard-fail.
Prerequisites
- The Day 61 lesson (read it first — it explains PEP 8, PEP 257, PEP 20, type hints, and the tools).
- Days 57–60: functions, modules and imports, and the standard library.
- Day 49: the shape of a real program — named functions, a
main(), and theif __name__ == "__main__":guard. - A text editor and a terminal. No experience beyond this course 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.9+ and bash).
- Windows — use WSL and follow the Linux path, or substitute
pythonforpython3if that is how Python is exposed. The scripts are pure standard-library Python and behave identically everywhere.
Hardware requirements
Any computer that runs Python 3. The scripts do a handful of arithmetic operations on the numbers you pass; they need no special memory, disk, or GPU.
Required software
python3(3.9 or newer; tested on 3.14.0).bashfor the test runner (preinstalled on macOS and Linux).- Standard library only — just
sys. No packages to install. Seerequirements/README.md.
Free and open-source options
Everything here is free and open source: Python, bash, and the standard library. The optional style tools introduced in the lesson — Black (formatter) and Ruff or flake8 (linter) — are also free and open source, but they are not required: the test suite skips their checks cleanly when they are absent. 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-061-writing-readable-code
python3 --version # confirm Python 3.9+ is available
Optionally, to try the style tools: python3 -m pip install black ruff.
File structure
day-061-writing-readable-code/
├── README.md ← you are here
├── metadata.yml ← machine-readable lab metadata
├── starter/
│ ├── messy.py ← YOUR working file — refactor it for readability
│ └── refactor-worksheet.md ← the five numbered refactoring steps + notes
├── examples/
│ └── report.py ← the clean, readable target (same behaviour)
├── tests/
│ └── run_tests.sh ← behaviour + readability checks (optional style tools skipped cleanly)
├── expected-output/
│ ├── sample-run.txt ← real captured runs (messy == clean, byte for byte)
│ ├── test-run.txt ← real captured test-suite run
│ └── 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 that the messy starter already works.
python3 starter/messy.py 70 85 90 55 60
## 2. See the clean target — and prove it behaves identically.
python3 examples/report.py 70 85 90 55 60
diff <(python3 starter/messy.py 70 85 90 55 60) <(python3 examples/report.py 70 85 90 55 60) && echo IDENTICAL
## 3. Establish the safety net BEFORE you touch anything.
bash tests/run_tests.sh
## 4. Refactor starter/messy.py, one step at a time (see the worksheet),
## running the tests after each step.
## 5. Optional: if you installed them, auto-format and lint.
black starter/messy.py # skips gracefully if not installed
ruff check starter/messy.py # or: flake8 starter/messy.py
## 6. Confirm behaviour is unchanged and readability now passes too.
bash tests/run_tests.sh
What the commands do
python3 starter/messy.py 70 85 90 55 60— runs the messy-but-working program: it parses the numbers, then prints count, mean, median, min, max, population standard deviation, and the percentage passing (≥ 60).python3 examples/report.py 70 85 90 55 60— runs the clean reference, which produces the same output from small, named, typed, documented functions.diff <(...) <(...) && echo IDENTICAL— proves the two versions are byte-for-byte identical: refactoring changed readability, not behaviour.bash tests/run_tests.sh— characterises the behaviour with a golden output, asserts both the starter and the reference match it (and the empty-input and even-count-median paths), checks the reference for readability markers (type hints, docstrings, ≥ 4 functions, no single-letter function names), and — once your starter is refactored — holds it to the same standard. If Black or Ruff/flake8 are installed it runs them as a bonus; if not, it skips those checks. Exits 0 only if every non-skipped check passes.black/ruff— the optional formatter and linter from the lesson; both skip gracefully when not installed.
Expected output
See expected-output/sample-run.txt — a real
captured session. For the input 70 85 90 55 60 both versions print:
count: 5
mean: 72.00
median: 70.00
min: 55.00
max: 90.00
stdev: 13.64
passing: 80.0%
The tool is deterministic, so your output will match.
expected-output/FIELDS.md lists the required
behaviour for every input on every platform.
Validation steps
python3 starter/messy.py 70 85 90 55 60prints the seven summary lines above and exits 0.diff <(python3 starter/messy.py 70 85 90 55 60) <(python3 examples/report.py 70 85 90 55 60)prints nothing (the two are identical), and the&& echo IDENTICALfires.bash tests/run_tests.shreports10 checks, 0 failure(s), 2 skipped.before you refactor.- Refactor
starter/messy.pyfollowing the worksheet, testing after each step; behaviour must stay identical the whole way. bash tests/run_tests.shnow reports14 checks, 0 failure(s), 2 skipped.— the readability checks now apply to your cleaned-up starter too.
Tests
bash tests/run_tests.sh
Expected final line while the starter is still messy: 10 checks, 0 failure(s), 2 skipped. Once you complete the refactor, the suite additionally
holds your starter to the readability standard, giving 14 checks, 0 failure(s), 2 skipped. The two skips are the optional Black and Ruff/flake8
checks, which are not installed by default; they never turn the suite red. The
command exits 0 on success and non-zero on any real failure, so it can run in
CI. A full captured run is in
expected-output/test-run.txt.
Cleanup
The scripts write no files, so there is nothing to delete. To reset your refactor and start over, restore the starter from git:
git checkout -- starter/messy.py
Troubleshooting
See troubleshooting.md for the full list: python vs
python3, tests going red mid-refactor, the golden output not matching (sample
vs population standard deviation), old-Python list[float] syntax, why the
style-tool checks say "skip", the module import path, and permissions.
Security notes
See security.md. Short version: the tool makes no network calls,
needs no privileges, and writes no files. It parses numbers with float(),
never eval(). And readable code is safer code — a reviewer can only catch
what they can see, which is exactly why names, small functions, docstrings, and
type hints matter.
Extension exercises
- Add a sample standard deviation function (
divide by n - 1) alongside the population one, with a clear name and docstring explaining the difference — and a test that pins its value so it never drifts. - Add a
--help-style usage line printed to standard error (and exit 2) when a value cannot be parsed as a number, instead of lettingfloat()raise — turning a crash into a readable error message. - Install Black and Ruff and run them on both files; read Ruff's output, understand each rule it flags, and fix or justify each one in a comment.
- Write a short
tests/test_report.pythat importsmean,median, andpopulation_stdevfrom the reference and asserts their values on a known list, printingall tests passedonly if every assertion holds.
Navigation
- Previous day: Day 60 — A Tour of the Standard Library
(
labs/sections/programming-with-python/day-060-a-tour-of-the-standard-library/). - Next day: Day 62 — Recursion
(
labs/sections/programming-with-python/day-062-recursion/, to be written). - Week 9 theme: Functions and Program Design — writing code that is not just correct but clear, reusable, and ready for review.
Expected output
FIELDS.md
# Expected output — Day 061 lab
These are real captured runs from the authoring machine (macOS, Apple
Silicon, Python 3.14.0, bash 3.2, 2026-07-13). The score summariser is
deterministic: given the same numbers it prints the same report and the same
exit code on every platform Python 3 runs on. The whole point of the lab is
that the messy starter and the clean reference produce **identical** output —
refactoring changes readability, never behaviour.
## Files
- `sample-run.txt` — the messy starter and the clean reference driven on the
same input (byte-for-byte identical, confirmed with `diff`), plus an
even-count median, a floating-point set, a single value, the empty-input
path, and a `python3 -c` import of one function.
- `test-run.txt` — a real run of `bash tests/run_tests.sh`. The first block is
the suite with the starter still messy (10 checks, 2 skipped); the second
block is the extra readability checks that run once the starter has been
refactored (14 checks, 2 skipped).
## Required behaviour on every platform
For the input `70 85 90 55 60`, every correct version of the tool must print
exactly these lines and exit 0:
| Line | Value | Why |
| --- | --- | --- |
| `count: 5` | number of scores | `len(scores)` |
| `mean: 72.00` | arithmetic mean | `360 / 5`, two decimals |
| `median: 70.00` | middle of the sorted list | sorted `[55,60,70,85,90]`, index 2 |
| `min: 55.00` | smallest score | `min(scores)` |
| `max: 90.00` | largest score | `max(scores)` |
| `stdev: 13.64` | population standard deviation | `sqrt(930/5) = sqrt(186)` |
| `passing: 80.0%` | share at or above the pass mark 60 | 4 of 5, one decimal |
Other captured cases (reference tool):
| Command | Key output | Exit code |
| --- | --- | --- |
| `report.py 70 85 90 55` | `median: 77.50` (even count → mean of two middle) | 0 |
| `report.py 100` | `stdev: 0.00`, `passing: 100.0%` | 0 |
| `report.py` (no args) | `no data` (stderr path in spirit; printed plainly here) | 1 |
## Skipped-tool behaviour (important)
The optional Black and Ruff/flake8 checks are **skipped, not failed**, when
those tools are not installed — which is the case on a plain Python install,
including the authoring machine (see the two `skip:` lines in
`test-run.txt`). A missing optional tool must never turn the suite red. If you
install them (`python3 -m pip install black ruff`), the same command runs
them on the reference and reports two extra `ok:` lines instead of skips.
## Platform notes
- The only visible difference between platforms is the shell prompt (`$`)
shown before each command; the program's own output is identical.
- Scores may be integers or decimals; they are parsed with `float()`, so
`90` and `90.0` are equivalent and always print with two decimals.
- The tool writes no files and makes no network calls; there is nothing to
clean up.
sample-run.txt
$ python3 starter/messy.py 70 85 90 55 60
count: 5
mean: 72.00
median: 70.00
min: 55.00
max: 90.00
stdev: 13.64
passing: 80.0%
$ python3 examples/report.py 70 85 90 55 60
count: 5
mean: 72.00
median: 70.00
min: 55.00
max: 90.00
stdev: 13.64
passing: 80.0%
$ diff <(python3 starter/messy.py 70 85 90 55 60) <(python3 examples/report.py 70 85 90 55 60) && echo IDENTICAL
IDENTICAL
$ python3 examples/report.py 70 85 90 55 ; echo "exit: $?"
count: 4
mean: 75.00
median: 77.50
min: 55.00
max: 90.00
stdev: 13.69
passing: 75.0%
exit: 0
$ python3 examples/report.py 88.5 92 79.5 61 45
count: 5
mean: 73.20
median: 79.50
min: 45.00
max: 92.00
stdev: 17.73
passing: 80.0%
$ python3 examples/report.py 100
count: 1
mean: 100.00
median: 100.00
min: 100.00
max: 100.00
stdev: 0.00
passing: 100.0%
$ python3 examples/report.py ; echo "exit: $?"
no data
exit: 1
$ python3 -c "import sys; sys.path.insert(0,'examples'); from report import median; print(median([1,2,3,4]))"
2.5
test-run.txt
Testing the clean reference (examples/report.py) ...
ok: reference: golden report matches, exit 0
ok: reference: empty input prints 'no data', exit 1
ok: reference: even-count median is 77.50
ok: reference: uses type hints (->)
ok: reference: has docstrings
ok: reference: decomposed into >= 4 functions (7)
ok: reference: no single-letter function names
Testing the starter (starter/messy.py) ...
ok: starter: golden report matches, exit 0
ok: starter: empty input prints 'no data', exit 1
ok: starter: even-count median is 77.50
Note: starter/messy.py is not refactored yet — checking behaviour only.
Refactor it (see starter/refactor-worksheet.md), then rerun to be
held to the readability standard too.
Optional style tools (skipped cleanly if not installed) ...
skip: black (tool not installed — optional)
skip: ruff / flake8 (tool not installed — optional)
10 checks, 0 failure(s), 2 skipped.
--- After completing the refactor of starter/messy.py, the same command
--- holds the cleaned-up starter to the readability standard as well:
Testing the starter (starter/messy.py) ...
ok: starter: golden report matches, exit 0
ok: starter: empty input prints 'no data', exit 1
ok: starter: even-count median is 77.50
Starter looks refactored — applying the readability standard to it too.
ok: starter: uses type hints (->)
ok: starter: has docstrings
ok: starter: decomposed into >= 4 functions (7)
ok: starter: no single-letter function names
14 checks, 0 failure(s), 2 skipped.
Source files
examples/report.py (2673 bytes)
#!/usr/bin/env python3
"""Summarise a list of numeric scores passed as command-line arguments.
This is the readable target of the Day 61 lab: the same behaviour as
``starter/messy.py``, but rewritten in small, named, type-hinted, documented
functions that follow PEP 8. It prints the count, mean, median, minimum,
maximum, population standard deviation, and the percentage of scores at or
above the pass mark.
All input comes from ``sys.argv``, so the tool can be scripted and tested
without a human at the keyboard:
python3 report.py 70 85 90 55 60
"""
from __future__ import annotations
import sys
# A score at or above this mark counts as "passing". Naming the number gives
# it meaning and one place to change it, instead of a bare 60 in the code.
PASS_MARK = 60.0
def parse_scores(raw_values: list[str]) -> list[float]:
"""Convert the raw command-line strings into a list of floats."""
return [float(value) for value in raw_values]
def mean(scores: list[float]) -> float:
"""Return the arithmetic mean of a non-empty list of scores."""
return sum(scores) / len(scores)
def median(scores: list[float]) -> float:
"""Return the middle score, or the mean of the two middle scores."""
ordered = sorted(scores)
count = len(ordered)
middle = count // 2
if count % 2 == 1:
return ordered[middle]
return (ordered[middle - 1] + ordered[middle]) / 2
def population_stdev(scores: list[float], average: float) -> float:
"""Return the population standard deviation around a known average."""
variance = sum((score - average) ** 2 for score in scores) / len(scores)
return variance ** 0.5
def passing_rate(scores: list[float]) -> float:
"""Return the percentage of scores at or above PASS_MARK."""
passing = sum(1 for score in scores if score >= PASS_MARK)
return 100 * passing / len(scores)
def format_report(scores: list[float]) -> str:
"""Build the multi-line summary report for a non-empty list of scores."""
average = mean(scores)
lines = [
f"count: {len(scores)}",
f"mean: {average:.2f}",
f"median: {median(scores):.2f}",
f"min: {min(scores):.2f}",
f"max: {max(scores):.2f}",
f"stdev: {population_stdev(scores, average):.2f}",
f"passing: {passing_rate(scores):.1f}%",
]
return "\n".join(lines)
def main(argv: list[str]) -> int:
"""Parse scores from argv, print the report, and return an exit code."""
scores = parse_scores(argv)
if not scores:
print("no data")
return 1
print(format_report(scores))
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
metadata.yml (825 bytes)
lesson_id: D061
day: 61
kind: python-program
languages: [python]
setup_commands:
- cd labs/sections/programming-with-python/day-061-writing-readable-code
- python3 --version
run_commands:
- python3 starter/messy.py 70 85 90 55 60
- python3 examples/report.py 70 85 90 55 60
- 'diff <(python3 starter/messy.py 70 85 90 55 60) <(python3 examples/report.py 70 85 90 55 60)'
test_commands:
- bash tests/run_tests.sh
cleanup_commands:
- 'git checkout -- starter/messy.py # optional: reset your refactor'
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 -> 10 checks, 0 failure(s), 2 skipped, exit 0 (unrefactored starter); 14 checks, 0 failure(s), 2 skipped once the starter is refactored'
requirements/README.md (1426 bytes)
# Dependencies — Day 061 lab
**Python 3 only. No third-party packages required.**
- `python3` (3.9 or newer; tested on 3.14.0). The reference uses
`list[float]` type-hint syntax, which reads cleanly on 3.9+. 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 — `sys` and nothing else. There is
deliberately no `requirements.txt`: refactoring for readability needs no
libraries at all.
Check your Python is present and new enough:
```bash
python3 --version
```
## Optional style tools (not required)
The lesson introduces a **formatter** (Black) and a **linter** (Ruff, or the
older flake8). They make style automatic, but they are entirely optional here:
the test suite runs with or without them, and simply **skips** their checks
when they are absent. If you want to try them:
```bash
python3 -m pip install black ruff # optional, one-time
black examples/report.py # auto-format in place
ruff check examples/report.py # report style/lint issues
```
If `pip` is restricted on your machine, skip this entirely — everything the
lab asserts works on a plain Python install with nothing downloaded. Windows
users: run the commands inside WSL, or use `python` in place of `python3` if
that is how Python is exposed on your system.
starter/messy.py (1058 bytes)
# Refactor THIS file for readability without changing what it does.
# The numbered steps are in starter/refactor-worksheet.md. After every step,
# run bash tests/run_tests.sh and confirm the output is still identical.
# It works today; your job is to make it readable, not to change its behaviour.
import sys
def d(a):
l=[]
for i in a:
l.append(float(i))
if len(l)==0:
print("no data")
return 1
t=0
for i in l: t=t+i
m=t/len(l)
s=sorted(l)
if len(l)%2==1:
md=s[len(l)//2]
else:
md=(s[len(l)//2-1]+s[len(l)//2])/2
v=0
for i in l:
v=v+(i-m)**2
v=v/len(l)
sd=v**0.5
p=0
for i in l:
if i>=60: p=p+1
print("count: "+str(len(l)))
print("mean: "+format(m,".2f"))
print("median: "+format(md,".2f"))
print("min: "+format(s[0],".2f"))
print("max: "+format(s[-1],".2f"))
print("stdev: "+format(sd,".2f"))
print("passing: "+format(100*p/len(l),".1f")+"%")
return 0
if __name__=="__main__":
sys.exit(d(sys.argv[1:]))
starter/refactor-worksheet.md (2942 bytes)
# Refactor worksheet — turn `messy.py` into readable code
`starter/messy.py` works: it prints a summary of the numbers you pass it. It
is also hard to read — one giant function called `d`, single-letter variables,
no docstrings, no type hints, a bare magic number, and cramped formatting.
Your job is to make it **readable without changing what it does**. The test
suite is your safety net: `bash tests/run_tests.sh` passes now and must keep
passing after every step.
The golden rule of refactoring: **one small change, then run the tests.** If
they still pass, keep going; if they fail, undo that one change and try again.
Never rename and restructure and retype all at once.
## The five steps (do them in order, testing after each)
1. **Rename for meaning.** Give every name a job title. `d` → `main`;
`a` → the raw argument strings; `l` → `scores`; `t` → a running `total`;
`m` → `average`; `s` → `ordered`; `md` → `median`; `sd` → `stdev`;
`p` → the `passing` count; `i` → `score` (or `value`). Run the tests.
2. **Kill the magic number.** The bare `60` is the pass mark. Add a module
constant near the top — `PASS_MARK = 60.0` — and use it in the comparison.
Now the number has a name and one place to change. Run the tests.
3. **Decompose into small functions.** Pull each distinct job out of the giant
function into its own function: `parse_scores`, `mean`, `median`,
`population_stdev`, `passing_rate`, and a `format_report` that assembles the
lines. Leave `main` as a short conductor that parses input, handles the
empty case, and prints the report. Run the tests after each extraction.
4. **Document and type.** Give the module a top docstring (what the file does)
and each function a one-line docstring that says **why / what it returns**,
not a play-by-play of the code. Add type hints to every function signature:
`def mean(scores: list[float]) -> float:`. Run the tests.
5. **Format and (optionally) lint.** Put blank lines between functions, spaces
around operators (`t=t+i` → `total += score`), and one statement per line.
If you have them installed, run `black messy.py` to auto-format and
`ruff check messy.py` (or `flake8`) to catch leftovers; if you do not, the
suite skips those checks cleanly. Run the tests one last time.
When you are done, `starter/messy.py` should read like
`examples/report.py` — and `bash tests/run_tests.sh` should report 14 checks,
0 failures (plus the readability checks now applied to your starter too).
## Before/after notes (fill these in)
- **Hardest name to choose, and what you picked:**
- **A function you extracted, and the one job it now does:**
- **One comment you deleted because the code already said it, or one you kept
because it explained *why*:**
- **Did the tests ever go red mid-refactor? What did you change to fix it?**
- **Final test line (`N checks, 0 failure(s), M skipped.`):**
tests/run_tests.sh (5740 bytes)
#!/usr/bin/env bash
# Tests for the Day 061 lab — "Refactor for Readability". Run from the lab
# directory:
# bash tests/run_tests.sh
#
# The central claim of refactoring is: behaviour does not change, only
# readability does. So these tests characterise the behaviour of the score
# summariser with a fixed golden output, then assert that BOTH the messy
# starter and the clean reference produce exactly that output and the same
# exit codes. They pass before you refactor (messy starter) and after you
# refactor (your cleaned-up starter) — that is the whole point.
#
# The suite also checks the reference for the readability markers this lesson
# teaches (type hints, docstrings, small functions, a named constant), and
# once your starter is refactored it holds it to the same standard.
#
# Finally, if a formatter (Black) or linter (Ruff / flake8) happens to be
# installed, it runs them on the reference as a bonus; if they are not
# installed, those checks are SKIPPED, never failed — the lab must run on a
# plain Python install with nothing extra to download.
#
# No network, non-interactive. Exits 0 only if every non-skipped check passes.
set -u
export PYTHONDONTWRITEBYTECODE=1
lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ref="${lab_dir}/examples/report.py"
starter="${lab_dir}/starter/messy.py"
failures=0
checks=0
skips=0
# The golden behaviour: for the input below, every correct version of the
# tool must print exactly these seven lines and exit 0.
golden_input=(70 85 90 55 60)
golden_output="count: 5
mean: 72.00
median: 70.00
min: 55.00
max: 90.00
stdev: 13.64
passing: 80.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
}
skip() {
echo " skip: $1 (tool not installed — optional)"
skips=$((skips + 1))
}
# behaviour_checks <label-prefix> <script>
# Asserts the golden output + exit 0, the empty-input path, and an even-count
# median — the behaviour that refactoring must preserve.
behaviour_checks() {
local name="$1" script="$2"
local out code
out="$(python3 "${script}" "${golden_input[@]}")"
code=$?
if [ "${code}" -eq 0 ] && [ "${out}" = "${golden_output}" ]; then
check "${name}: golden report matches, exit 0" "yes"
else
check "${name}: golden report matches, exit 0" "no"
echo " (exit ${code}; output was:)"
printf '%s\n' "${out}" | sed 's/^/ /'
fi
out="$(python3 "${script}" 2>&1)"
code=$?
if [ "${code}" -eq 1 ] && [ "${out}" = "no data" ]; then
check "${name}: empty input prints 'no data', exit 1" "yes"
else
check "${name}: empty input prints 'no data', exit 1" "no"
echo " (exit ${code}; output: ${out})"
fi
# Even count: sorted [55,70,85,90] -> median (70+85)/2 = 77.50.
out="$(python3 "${script}" 70 85 90 55)"
if printf '%s\n' "${out}" | grep -qF "median: 77.50"; then
check "${name}: even-count median is 77.50" "yes"
else
check "${name}: even-count median is 77.50" "no"
echo " (output: ${out})"
fi
}
# readability_checks <label-prefix> <script>
# The markers this lesson teaches: type hints, docstrings, several small
# functions, and a named constant instead of a bare magic number.
readability_checks() {
local name="$1" script="$2"
grep -q -- '->' "${script}" \
&& check "${name}: uses type hints (->)" "yes" \
|| check "${name}: uses type hints (->)" "no"
grep -q '"""' "${script}" \
&& check "${name}: has docstrings" "yes" \
|| check "${name}: has docstrings" "no"
local func_count
func_count="$(grep -c '^def ' "${script}")"
if [ "${func_count}" -ge 4 ]; then
check "${name}: decomposed into >= 4 functions (${func_count})" "yes"
else
check "${name}: decomposed into >= 4 functions (${func_count})" "no"
fi
if grep -qE '^def [a-z]\(' "${script}"; then
check "${name}: no single-letter function names" "no"
else
check "${name}: no single-letter function names" "yes"
fi
}
echo "Testing the clean reference (examples/report.py) ..."
behaviour_checks "reference" "${ref}"
readability_checks "reference" "${ref}"
echo "Testing the starter (starter/messy.py) ..."
# Behaviour must hold whether or not the learner has refactored yet.
behaviour_checks "starter" "${starter}"
if grep -qE '^def d\(' "${starter}"; then
echo "Note: starter/messy.py is not refactored yet — checking behaviour only."
echo " Refactor it (see starter/refactor-worksheet.md), then rerun to be"
echo " held to the readability standard too."
else
echo "Starter looks refactored — applying the readability standard to it too."
readability_checks "starter" "${starter}"
fi
# --- Optional formatter / linter (bonus; never a hard failure) ---
echo "Optional style tools (skipped cleanly if not installed) ..."
if command -v black >/dev/null 2>&1; then
if black --check --quiet "${ref}" 2>/dev/null; then
check "black --check: reference is already formatted" "yes"
else
check "black --check: reference is already formatted" "no"
echo " (run 'black ${ref}' to see the suggested formatting)"
fi
else
skip "black"
fi
if command -v ruff >/dev/null 2>&1; then
if ruff check "${ref}" >/dev/null 2>&1; then
check "ruff check: reference is clean" "yes"
else
check "ruff check: reference is clean" "no"
fi
elif command -v flake8 >/dev/null 2>&1; then
if flake8 --max-line-length=100 "${ref}" >/dev/null 2>&1; then
check "flake8: reference is clean" "yes"
else
check "flake8: reference is clean" "no"
fi
else
skip "ruff / flake8"
fi
echo
echo "${checks} checks, ${failures} failure(s), ${skips} skipped."
[ "${failures}" -eq 0 ]
Troubleshooting
Troubleshooting — Day 061 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 (you want 3.9 or newer for the list[float] hints).
The tests failed after I renamed something
That is the safety net doing its job — you changed behaviour, not just a name.
Undo the last single change and run bash tests/run_tests.sh again to get back
to green, then redo it more carefully. The most common causes are a typo that
renamed one use of a variable but not another, or accidentally changing the
order of operations while "tidying" an expression. Refactor in the smallest
steps you can, testing after each.
The golden output does not match after my refactor
The tool must print exactly the seven lines in
expected-output/FIELDS.md for the input 70 85 90 55 60. Watch for changes
that alter numbers or formatting: switching population standard deviation
(divide by n) to sample standard deviation (divide by n - 1) changes
stdev; changing the pass mark changes passing; using round() instead of
:.2f formatting can differ at the last digit. Readability refactoring keeps
all of these identical — only the names and structure change.
SyntaxError mentioning list[float]
You are on a Python older than 3.9. Either upgrade, or write the hints as
from typing import List and List[float]. The reference targets 3.9+, where
the built-in list[...] form works directly.
The black / ruff checks say "skip"
That is expected and fine — those tools are optional and are not installed by
default. The suite skips them cleanly and still passes. If you want them, run
python3 -m pip install black ruff; if pip is restricted on your machine,
ignore this entirely, as nothing the lab asserts depends on them.
ModuleNotFoundError: No module named 'report' in the import check
Python looks for modules on its search path (sys.path), which does not
include the examples/ subfolder by default. The import one-liner adds it
first:
python3 -c "import sys; sys.path.insert(0,'examples'); from report import median; print(median([1,2,3,4]))"
Run it from the lab directory (the folder that contains examples/).
bash: tests/run_tests.sh: Permission denied
Run it through bash explicitly, as the README shows: bash tests/run_tests.sh.
You do not need to chmod +x anything.
The starter still prints NotImplementedError — no, it does not
Unlike some labs, this starter is a complete, working program from the
start — the exercise is to refactor it, not to fill in blanks. If it ever
raises an error, you introduced it during the refactor; restore the original
with git checkout -- starter/messy.py and begin again in smaller steps.
Security notes
Security notes — Day 061 lab
-
What the tool does: reads numbers from the command line, computes summary statistics, and prints them. It makes no network connections, needs no privileges, writes no files, and touches nothing on disk. The test runner only reads the two scripts and runs them; it creates no temporary files.
-
Refactoring must not change behaviour — including security behaviour. The discipline this lab teaches (small safe steps, tests after each) is exactly how you avoid accidentally introducing a bug while "just cleaning up." A refactor that quietly changes what the code does is a classic source of regressions, some of them security-relevant. The test suite exists so that a behaviour change shows up immediately as a red test.
-
Parse input; do not execute it. Scores enter as strings and are converted with
float(), which can only ever produce a number or raise aValueError. Never reach foreval()to "read a number" —eval()executes its argument as Python, so a hostile value could run arbitrary code.float()is the safe parser, and it is what both versions of the tool use. -
Readable code is safer code. A reviewer can only catch a security problem they can see. Meaningful names, small functions, docstrings, and type hints are not just tidiness — they are what make a bug or an unsafe call visible in review. Sloppy code hides mistakes from humans and from tools; the whole point of this lesson is to stop doing that.
-
Optional tools are optional. If you choose to install Black or Ruff, get them from the official Python Package Index with
python3 -m pip install, and read what any tool does before running it. The lab never requires them and never fails when they are missing. -
Reading before running: every file in this lab is short and commented. Read
starter/messy.py,examples/report.py, andtests/run_tests.shbefore running them. Running unread scripts is one of the most common ways developers get compromised; the course's rule is that every lab script is small enough to read and understand first.