Skip to content

Commit e68ccab

Browse files
travisjneumanclaude
andcommitted
feat: add step-by-step WALKTHROUGH.md for 9 key projects (levels 9-10, elite track)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72bc4cf commit e68ccab

9 files changed

Lines changed: 2077 additions & 0 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
# Algorithms and Complexity Lab — Step-by-Step Walkthrough
2+
3+
[<- Back to Project README](./README.md) · [Solution](./SOLUTION.md)
4+
5+
## Before You Start
6+
7+
Read the [project README](./README.md) first. Try to solve it on your own before following this guide. This project teaches systematic analysis of input data with deterministic processing and structured output. Spend at least 30 minutes attempting it independently.
8+
9+
## Thinking Process
10+
11+
This project looks simple on the surface: read a CSV-like input file, classify each line, compute a summary, and write JSON output. But the engineering decisions embedded in it are what separate production-grade code from scripts. Every function is deterministic (same input always produces same output). Every boundary is validated (missing file, empty file, malformed line). Every output is structured for downstream consumers (JSON with stable keys).
12+
13+
The mental model is a **data pipeline**: `load -> validate -> transform -> summarize -> persist`. Each stage is a pure function (except I/O at the edges). This separation means you can test the transform and summarize logic without touching the filesystem, and you can swap the input format (CSV, JSON, database) without changing the core logic.
14+
15+
The key engineering constraint is **reproducibility**. The `run_id` parameter creates traceability: you can tell which invocation produced which output. The deterministic functions mean two runs with the same input produce the same output (except for the timestamp). This is essential for benchmarking, where you need to compare runs before and after an optimization.
16+
17+
## Step 1: Parse CLI Arguments
18+
19+
**What to do:** Write `parse_args()` using `argparse` with three arguments: `--input` (required path to input data), `--output` (required path to output JSON), and `--run-id` (optional identifier, defaults to "manual-run").
20+
21+
**Why:** CLI arguments make the script composable. Instead of hardcoding paths, you can run `python project.py --input data/sample_input.txt --output data/output_summary.json --run-id smoke-check` and get deterministic, traceable results. The `run-id` supports automation: a CI pipeline can pass a build number, a benchmark suite can pass a version tag.
22+
23+
```python
24+
def parse_args() -> argparse.Namespace:
25+
parser = argparse.ArgumentParser(description="Algorithms and Complexity Lab")
26+
parser.add_argument("--input", required=True, help="Path to input text data")
27+
parser.add_argument("--output", required=True, help="Path to output JSON summary")
28+
parser.add_argument("--run-id", default="manual-run", help="Optional run identifier")
29+
return parser.parse_args()
30+
```
31+
32+
Both `--input` and `--output` are `required=True`. This is intentional -- the script should fail immediately if the caller forgets to specify where to read from or write to, rather than silently using a default that might not exist.
33+
34+
**Predict:** What happens if you run `python project.py` with no arguments? What error does `argparse` produce?
35+
36+
## Step 2: Load and Validate Input Lines
37+
38+
**What to do:** Write `load_lines()` that reads a file, strips whitespace, filters empty lines, and rejects empty datasets.
39+
40+
**Why:** Input validation is the first line of defense. A missing file should produce a clear `FileNotFoundError`, not a cryptic traceback from a downstream function. An empty file should produce a clear `ValueError`, not a division-by-zero error when computing averages. Fail fast, fail clearly.
41+
42+
```python
43+
def load_lines(input_path: Path) -> list[str]:
44+
if not input_path.exists():
45+
raise FileNotFoundError(f"input file not found: {input_path}")
46+
47+
lines = [
48+
line.strip()
49+
for line in input_path.read_text(encoding="utf-8").splitlines()
50+
if line.strip()
51+
]
52+
if not lines:
53+
raise ValueError("input file contains no usable lines")
54+
return lines
55+
```
56+
57+
Three details matter:
58+
59+
- **`encoding="utf-8"` is explicit.** On Windows, the default encoding is often `cp1252`, which can silently corrupt non-ASCII characters. Always specify UTF-8 for cross-platform consistency.
60+
- **`line.strip()` is applied twice** -- once in the filter condition (`if line.strip()`) and once in the output list. This ensures both filtering and normalization happen, removing trailing whitespace and `\r\n` line endings.
61+
- **Empty lines are silently dropped**, but an entirely empty file raises an error. This is the right balance: a stray blank line is normal; a completely empty input is probably a mistake.
62+
63+
**Predict:** If the input file contains `"alpha,10,ok\n\n\nbeta,7,warn\n"`, how many lines does `load_lines()` return?
64+
65+
## Step 3: Transform Each Line into Structured Data
66+
67+
**What to do:** Write `classify_line()` that splits a CSV-like line into three fields (name, score, severity) and adds a computed `is_high_risk` boolean.
68+
69+
**Why:** Raw text lines are not useful for analysis. Transforming each line into a structured dictionary with typed fields (integer score, boolean risk flag) enables downstream aggregation. The validation (exactly 3 comma-separated fields) catches malformed input before it causes confusing errors later.
70+
71+
```python
72+
def classify_line(line: str) -> dict[str, Any]:
73+
parts = [piece.strip() for piece in line.split(",")]
74+
if len(parts) != 3:
75+
raise ValueError(f"invalid line format (expected 3 comma fields): {line}")
76+
77+
name, score_raw, severity = parts
78+
score = int(score_raw)
79+
return {
80+
"name": name,
81+
"score": score,
82+
"severity": severity,
83+
"is_high_risk": severity in {"warn", "critical"} or score < 5,
84+
}
85+
```
86+
87+
The `is_high_risk` flag combines two conditions with `or`: either the severity is elevated ("warn" or "critical"), or the score is below 5. This creates a consistent risk lens that the summary can count without re-evaluating the raw data.
88+
89+
**Predict:** For the line `"gamma,2,critical"`, what does `classify_line()` return? Is it high risk, and for how many reasons?
90+
91+
## Step 4: Build the Summary Payload
92+
93+
**What to do:** Write `build_summary()` that takes the classified records, a project title, and a run ID, and produces a deterministic JSON-ready summary with counts and averages.
94+
95+
**Why:** The summary is the deliverable. It contains everything a downstream consumer needs: how many records were processed, how many are high risk, the average score, and the raw records for debugging. The `run_id` and `project_title` provide traceability -- you can match any output file back to the exact invocation that produced it.
96+
97+
```python
98+
def build_summary(
99+
records: list[dict[str, Any]],
100+
project_title: str,
101+
run_id: str,
102+
) -> dict[str, Any]:
103+
high_risk_count = sum(1 for record in records if record["is_high_risk"])
104+
avg_score = round(
105+
sum(record["score"] for record in records) / len(records), 2
106+
)
107+
108+
return {
109+
"project_title": project_title,
110+
"run_id": run_id,
111+
"generated_utc": datetime.now(timezone.utc).isoformat(),
112+
"record_count": len(records),
113+
"high_risk_count": high_risk_count,
114+
"average_score": avg_score,
115+
"records": records,
116+
}
117+
```
118+
119+
Three details to notice:
120+
121+
- **`round(..., 2)` ensures consistent decimal places.** Without rounding, floating-point arithmetic might produce `6.333333333333333` instead of `6.33`.
122+
- **`datetime.now(timezone.utc)` uses timezone-aware UTC.** This avoids ambiguity about which timezone the timestamp is in -- essential for distributed systems.
123+
- **The records are included in the output.** This seems redundant, but it is invaluable for debugging: you can see exactly what was processed without re-running the pipeline.
124+
125+
**Predict:** Given records `[{"score": 10, "is_high_risk": False}, {"score": 3, "is_high_risk": True}]`, what is the `average_score`? What is `high_risk_count`?
126+
127+
## Step 5: Write Output and Orchestrate the Pipeline
128+
129+
**What to do:** Write `write_summary()` for file persistence and `main()` to orchestrate the full pipeline: parse args, load, transform, summarize, write.
130+
131+
**Why:** `write_summary()` handles directory creation (`parents=True`) so the script works even on first run when the output directory does not exist. The `main()` function is the orchestrator that connects all the pure functions into an end-to-end pipeline.
132+
133+
```python
134+
def write_summary(output_path: Path, payload: dict[str, Any]) -> None:
135+
output_path.parent.mkdir(parents=True, exist_ok=True)
136+
output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
137+
138+
139+
def main() -> int:
140+
args = parse_args()
141+
input_path = Path(args.input)
142+
output_path = Path(args.output)
143+
144+
lines = load_lines(input_path)
145+
records = [classify_line(line) for line in lines]
146+
147+
payload = build_summary(records, "Algorithms and Complexity Lab", args.run_id)
148+
write_summary(output_path, payload)
149+
150+
print(f"output_summary.json written to {output_path}")
151+
return 0
152+
```
153+
154+
The `main()` function returns an integer exit code (0 for success). The `if __name__ == "__main__": raise SystemExit(main())` pattern converts this into a proper process exit code, which CI systems and shell scripts use to detect success or failure.
155+
156+
**Predict:** If `load_lines()` raises `FileNotFoundError`, does `main()` return 0? What happens to the process exit code?
157+
158+
## Common Mistakes
159+
160+
| Mistake | Why It Happens | Fix |
161+
|---------|---------------|-----|
162+
| Forgetting `encoding="utf-8"` | Relying on platform default encoding | Always specify encoding explicitly for cross-platform safety |
163+
| `int(score_raw)` crashes on non-numeric input | No validation before conversion | Wrap in try/except or validate format before converting |
164+
| Division by zero in `avg_score` | Empty records list | `load_lines()` prevents this, but add a guard in `build_summary()` for safety |
165+
| Output directory does not exist | First run in a clean environment | `output_path.parent.mkdir(parents=True, exist_ok=True)` |
166+
| Timestamp varies between runs | Using `datetime.now()` without timezone | This is expected -- `generated_utc` is the only non-deterministic field |
167+
168+
## Testing Your Solution
169+
170+
```bash
171+
pytest -q
172+
```
173+
174+
Expected output:
175+
```text
176+
2 passed
177+
```
178+
179+
Test from the command line:
180+
181+
```bash
182+
python project.py --input data/sample_input.txt --output data/output_summary.json --run-id smoke-check
183+
```
184+
185+
Then inspect `data/output_summary.json`. It should contain 3 records (alpha, beta, gamma), with `high_risk_count: 2` (beta is "warn" and gamma is "critical" with score < 5), and `average_score: 6.33`.
186+
187+
## What You Learned
188+
189+
- **Deterministic pipelines** produce the same output for the same input. This enables benchmarking (compare before and after optimization), reproducibility (anyone can re-run and verify), and debugging (replay a failing run with the exact same data).
190+
- **Fail-fast validation** at input boundaries prevents confusing errors downstream. A clear "input file not found" message at the top of the pipeline is worth far more than a cryptic `KeyError` buried in a transform function.
191+
- **Structured output with traceability** (run ID, project title, UTC timestamp) turns a script into a tool. You can match any output file back to the invocation that produced it, which is essential for audit trails and production debugging.

0 commit comments

Comments
 (0)