Skip to content

Commit 8e37fe5

Browse files
travisjneumanclaude
andcommitted
feat: flesh out SOLUTION.md for all 15 level-0 projects
Replace skeleton placeholders with complete, pedagogically rich solution guides for every project in projects/level-0/: - Full working solution code with extensive WHY comments on every non-obvious line explaining design decisions - Design decisions tables with real trade-offs from each project - Alternative approaches with concrete code and when to prefer them - "What could go wrong" tables covering common beginner mistakes, edge cases, and their prevention strategies - Key takeaways connecting each project to future concepts and real-world applications Projects covered (01-15): Terminal Hello Lab, Calculator Basics, Temperature Converter, Yes/No Questionnaire, Number Classifier, Word Counter Basic, First File Reader, String Cleaner Starter, Daily Checklist Writer, Duplicate Line Finder, Simple Menu Loop, Contact Card Builder, Alarm Message Generator, Line Length Summarizer, Mini Toolkit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8263271 commit 8e37fe5

File tree

15 files changed

+2966
-0
lines changed

15 files changed

+2966
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# Solution: Level 0 / Project 01 - Terminal Hello Lab
2+
3+
> **STOP** — Have you attempted this project yourself first?
4+
>
5+
> Learning happens in the struggle, not in reading answers.
6+
> Spend at least 20 minutes trying before reading this solution.
7+
> If you are stuck, try the [Walkthrough](./WALKTHROUGH.md) first — it guides
8+
> your thinking without giving away the answer.
9+
10+
---
11+
12+
13+
## Complete solution
14+
15+
```python
16+
"""Level 0 project: Terminal Hello Lab.
17+
18+
Practice printing to the terminal, using variables, and
19+
understanding how Python sends text to your screen.
20+
21+
Concepts: print(), variables, string concatenation, f-strings, escape characters.
22+
"""
23+
24+
25+
# WHY greet is a function: Wrapping the greeting in a function makes it
26+
# reusable and testable. Tests can call greet("Ada") directly without
27+
# running the whole script or simulating user input.
28+
def greet(name: str) -> str:
29+
"""Build a personalised greeting string."""
30+
# WHY f-string: f"..." lets us embed variables directly inside the string.
31+
# It is cleaner than "Hello, " + name + "! Welcome to Python."
32+
return f"Hello, {name}! Welcome to Python."
33+
34+
35+
# WHY width defaults to 40: Default arguments let callers skip the parameter
36+
# when the common case is fine. A 40-character banner fits comfortably in
37+
# most terminals.
38+
def build_banner(title: str, width: int = 40) -> str:
39+
"""Create a decorative banner around a title."""
40+
# WHY "*" * width: String repetition creates a horizontal rule.
41+
# The * character repeated `width` times makes one border line.
42+
border = "*" * width
43+
44+
# WHY .center(): It pads the title with spaces so it sits in the middle.
45+
# This keeps the output visually balanced regardless of the title length.
46+
centered_title = title.center(width)
47+
48+
# WHY join with \n: We build all three lines and combine them with
49+
# newline characters so the function returns one complete string.
50+
return f"{border}\n{centered_title}\n{border}"
51+
52+
53+
# WHY build_info_card returns a dict: Dictionaries label each piece of data
54+
# with a key, making the output self-documenting. Code that receives the
55+
# dict can access card["name"] instead of guessing what index 0 means.
56+
def build_info_card(name: str, language: str, day: int) -> dict:
57+
"""Collect key facts into a dictionary."""
58+
return {
59+
"name": name,
60+
"language": language,
61+
"learning_day": day,
62+
# WHY call greet() here: Reusing the greet function avoids duplicating
63+
# the greeting logic. If the format changes, we only fix it once.
64+
"greeting": greet(name),
65+
}
66+
67+
68+
# WHY run_hello_lab exists: It groups all the "do stuff" steps into one
69+
# callable unit. The script's __main__ block stays tiny, and tests could
70+
# call this function to verify the full workflow.
71+
def run_hello_lab(name: str, day: int) -> dict:
72+
"""Execute the full hello-lab workflow and return results."""
73+
# --- Terminal output (side effects) ---
74+
banner = build_banner("TERMINAL HELLO LAB")
75+
print(banner)
76+
print() # WHY blank line: Visual breathing room between sections.
77+
78+
greeting = greet(name)
79+
print(greeting)
80+
81+
# WHY \t: The tab character indents the text, showing how escape
82+
# characters control formatting inside strings.
83+
print(f"\tDay {day} of your Python journey.")
84+
print()
85+
86+
# WHY \n inside the string: Demonstrates that escape characters work
87+
# inside f-strings too — this prints on two lines from one print() call.
88+
print("Fun fact: Python is named after Monty Python,\nnot the snake!")
89+
90+
# --- Build summary ---
91+
summary = build_info_card(name, "Python", day)
92+
return summary
93+
94+
95+
# WHY __name__ == "__main__": This guard means the code below only runs
96+
# when you execute the file directly (python project.py), NOT when
97+
# another file imports it. Tests import greet() and build_banner()
98+
# without triggering the interactive input prompts.
99+
if __name__ == "__main__":
100+
name = input("What is your name? ")
101+
day_text = input("What day of your Python journey is it? ")
102+
103+
# WHY int(): input() always returns a string. We need an integer
104+
# for the day number so we can do math with it later.
105+
day = int(day_text)
106+
107+
summary = run_hello_lab(name, day)
108+
109+
print("\n--- Your Info Card ---")
110+
# WHY .items(): Iterating over key-value pairs lets us print
111+
# every field without knowing the exact keys in advance.
112+
for key, value in summary.items():
113+
print(f" {key}: {value}")
114+
```
115+
116+
## Design decisions
117+
118+
| Decision | Why | Alternative considered |
119+
|----------|-----|----------------------|
120+
| `greet()` as a standalone function | Makes the greeting testable in isolation — tests call `greet("Ada")` without running the whole script | Inline the greeting with `print(f"Hello, {name}!")` directly — simpler but untestable |
121+
| `build_banner()` uses a `width` default parameter | Callers get a sensible 40-char banner without passing extra arguments, but can customise when needed | Hard-code the width to 40 — less flexible if the title is very long |
122+
| `build_info_card()` returns a `dict` | Keys like `"name"` and `"language"` make data self-documenting; any code can access fields by name | Return a tuple `(name, language, day)` — shorter but relies on positional order, which is fragile |
123+
| `run_hello_lab()` both prints and returns | Lets the interactive script show output AND lets tests inspect the returned dict | Print-only with no return — tests would have to capture stdout, which is harder for beginners |
124+
125+
## Alternative approaches
126+
127+
### Approach B: String concatenation instead of f-strings
128+
129+
```python
130+
def greet(name: str) -> str:
131+
# Using + to join strings instead of f-strings.
132+
return "Hello, " + name + "! Welcome to Python."
133+
134+
def build_banner(title: str, width: int = 40) -> str:
135+
border = "*" * width
136+
# Using .format() instead of f-strings.
137+
centered_title = "{:^{}}".format(title, width)
138+
return border + "\n" + centered_title + "\n" + border
139+
```
140+
141+
**Trade-off:** String concatenation with `+` is the most basic approach and works in all Python versions. However, f-strings (available since Python 3.6) are easier to read when mixing text and variables. You can see at a glance what the output looks like. The `.format()` method is a middle ground — more powerful than `+` but less readable than f-strings. For beginners, f-strings are the recommended default.
142+
143+
## What could go wrong
144+
145+
| Scenario | What happens | Prevention |
146+
|----------|-------------|------------|
147+
| User presses Enter without typing a name (empty string) | `greet("")` returns `"Hello, ! Welcome to Python."` — an awkward blank space | Add a guard: `if not name.strip(): name = "friend"` before calling `greet()` |
148+
| User types letters for the day number (e.g. "seven") | `int("seven")` raises `ValueError` and the program crashes | Wrap `int(day_text)` in a `try/except ValueError` and ask again |
149+
| User types a negative day number (e.g. "-3") | `int("-3")` succeeds, and the program says "Day -3" — technically wrong | Check `if day < 1:` and ask again or default to 1 |
150+
| User types only spaces as their name | `greet(" ")` returns `"Hello, ! Welcome to Python."` — looks messy | Use `.strip()` on the name and check if it is empty after stripping |
151+
152+
## Key takeaways
153+
154+
1. **Functions make code testable.** By wrapping logic in `greet()` and `build_banner()`, tests can verify each piece independently without simulating user input. This is why every project from here on uses functions.
155+
2. **f-strings are your go-to for mixing text and variables.** The syntax `f"Hello, {name}!"` is clearer than concatenation and will be used in nearly every Python project you encounter.
156+
3. **The `if __name__ == "__main__"` guard separates "library code" from "script code."** This pattern appears in every project going forward and becomes essential when you start importing modules in Level 1+.
157+
4. **Dictionaries bundle related data with meaningful labels.** The `build_info_card()` function previews a pattern you will use constantly — returning structured data from functions instead of just printing it.
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Solution: Level 0 / Project 02 - Calculator Basics
2+
3+
> **STOP** — Have you attempted this project yourself first?
4+
>
5+
> Learning happens in the struggle, not in reading answers.
6+
> Spend at least 20 minutes trying before reading this solution.
7+
> If you are stuck, try the [Walkthrough](./WALKTHROUGH.md) first — it guides
8+
> your thinking without giving away the answer.
9+
10+
---
11+
12+
13+
## Complete solution
14+
15+
```python
16+
"""Level 0 project: Calculator Basics.
17+
18+
A four-operation calculator that takes input from the user
19+
and computes results interactively.
20+
21+
Concepts: arithmetic operators, float/int conversion, input validation, functions.
22+
"""
23+
24+
25+
# WHY separate functions for each operation: Each function does exactly one
26+
# thing, making it trivial to test. assert add(2, 3) == 5 is crystal clear.
27+
def add(a: float, b: float) -> float:
28+
"""Return the sum of two numbers."""
29+
return a + b
30+
31+
32+
def subtract(a: float, b: float) -> float:
33+
"""Return the difference of two numbers."""
34+
return a - b
35+
36+
37+
def multiply(a: float, b: float) -> float:
38+
"""Return the product of two numbers."""
39+
return a * b
40+
41+
42+
def divide(a: float, b: float) -> float:
43+
"""Return the quotient of two numbers.
44+
45+
WHY check for zero? -- Dividing by zero crashes the program with
46+
ZeroDivisionError. Checking first lets us raise ValueError with
47+
a clear message that explains what went wrong.
48+
"""
49+
if b == 0:
50+
raise ValueError("Cannot divide by zero")
51+
return a / b
52+
53+
54+
def calculate(expression: str) -> dict:
55+
"""Parse a simple expression like '10 + 5' and return the result.
56+
57+
WHY split on spaces? -- We expect the format 'number operator number'.
58+
Splitting on whitespace gives us exactly three pieces to work with.
59+
"""
60+
parts = expression.strip().split()
61+
62+
# WHY check length: If the user typed "5" or "1 + 2 + 3", we cannot
63+
# parse it as a simple binary expression. Return an error dict
64+
# instead of crashing.
65+
if len(parts) != 3:
66+
return {"expression": expression.strip(), "error": "Expected format: number operator number"}
67+
68+
raw_a, operator, raw_b = parts
69+
70+
# WHY try/except: float() raises ValueError if the string is not a
71+
# number. Catching it lets us return a helpful error instead of
72+
# a scary traceback.
73+
try:
74+
a = float(raw_a)
75+
b = float(raw_b)
76+
except ValueError:
77+
return {"expression": expression.strip(), "error": f"Invalid numbers: {raw_a}, {raw_b}"}
78+
79+
# WHY a dict mapping operators to functions: This replaces a long
80+
# if/elif/else chain. Adding a new operator means adding one line
81+
# to the dict instead of another elif branch.
82+
operations = {
83+
"+": add,
84+
"-": subtract,
85+
"*": multiply,
86+
"/": divide,
87+
}
88+
89+
if operator not in operations:
90+
return {"expression": expression.strip(), "error": f"Unknown operator: {operator}"}
91+
92+
# WHY another try/except: divide() can raise ValueError for zero
93+
# division. We catch it here so calculate() always returns a dict,
94+
# never crashes.
95+
try:
96+
result = operations[operator](a, b)
97+
except ValueError as err:
98+
return {"expression": expression.strip(), "error": str(err)}
99+
100+
return {"expression": expression.strip(), "result": result}
101+
102+
103+
if __name__ == "__main__":
104+
print("=== Calculator ===")
105+
print("Type an expression like '10 + 5' or 'quit' to exit.\n")
106+
print("Supported operators: + - * /\n")
107+
108+
# WHY while True with break: This is the standard "loop until quit"
109+
# pattern. The loop runs forever; only the break statement exits it.
110+
while True:
111+
expression = input("Enter expression: ")
112+
113+
if expression.strip().lower() in ("quit", "exit", "q"):
114+
print("Goodbye!")
115+
break
116+
117+
result = calculate(expression)
118+
119+
if "error" in result:
120+
print(f" ERROR: {result['error']}")
121+
else:
122+
print(f" {result['expression']} = {result['result']}")
123+
124+
print()
125+
```
126+
127+
## Design decisions
128+
129+
| Decision | Why | Alternative considered |
130+
|----------|-----|----------------------|
131+
| Separate `add`, `subtract`, `multiply`, `divide` functions | Each is independently testable with a single `assert`. Tests read naturally: `assert add(2, 3) == 5` | One big `calculate()` that does everything — harder to test individual operations |
132+
| `operations` dict maps operator strings to functions | Adding a new operator (like `%` or `**`) requires one new dict entry instead of another `elif` branch | `if/elif/else` chain — works but gets long as operators grow; does not demonstrate dict-as-dispatch |
133+
| `calculate()` returns a dict with either `"result"` or `"error"` | The caller checks one key to know if it worked. No exceptions leak out of `calculate()` | Raise exceptions for errors — forces the caller to use try/except, which is more complex at Level 0 |
134+
| `divide()` raises `ValueError` instead of returning a special value | Exceptions are Python's standard way to signal errors. The caller in `calculate()` catches it and converts to an error dict | Return `None` or `float('inf')` — hides the error, and the caller may not notice |
135+
136+
## Alternative approaches
137+
138+
### Approach B: if/elif chain instead of dict dispatch
139+
140+
```python
141+
def calculate(expression: str) -> dict:
142+
parts = expression.strip().split()
143+
if len(parts) != 3:
144+
return {"expression": expression.strip(), "error": "Expected format: number operator number"}
145+
146+
raw_a, operator, raw_b = parts
147+
a = float(raw_a)
148+
b = float(raw_b)
149+
150+
# Direct if/elif chain — no dict needed.
151+
if operator == "+":
152+
result = a + b
153+
elif operator == "-":
154+
result = a - b
155+
elif operator == "*":
156+
result = a * b
157+
elif operator == "/":
158+
if b == 0:
159+
return {"expression": expression.strip(), "error": "Cannot divide by zero"}
160+
result = a / b
161+
else:
162+
return {"expression": expression.strip(), "error": f"Unknown operator: {operator}"}
163+
164+
return {"expression": expression.strip(), "result": result}
165+
```
166+
167+
**Trade-off:** The if/elif approach is easier for an absolute beginner to read because it uses no advanced concepts like "storing functions in a dict." However, the dict dispatch approach scales better — adding modulo or exponentiation is one line instead of another branch. In real codebases, dict dispatch is the standard pattern for this kind of routing.
168+
169+
## What could go wrong
170+
171+
| Scenario | What happens | Prevention |
172+
|----------|-------------|------------|
173+
| User types `10 / 0` | `divide()` raises `ValueError("Cannot divide by zero")`, caught by `calculate()` — returns error dict | Already handled; the outer try/except catches it |
174+
| User types `hello + world` | `float("hello")` raises `ValueError`, caught in the try/except — returns error dict | Already handled by the input validation block |
175+
| User types just `5` (no operator) | `parts` has length 1, not 3 — returns "Expected format" error | Already handled by the length check |
176+
| User types `10+5` (no spaces) | `split()` gives `["10+5"]` — length 1, returns format error | Document that spaces are required; alternatively, use regex to parse |
177+
| User types `10 + 5 + 3` | `parts` has length 5, not 3 — returns format error | Only binary expressions are supported; extending to multi-operand requires a proper parser |
178+
179+
## Key takeaways
180+
181+
1. **Dict dispatch replaces long if/elif chains.** Mapping `"+"` to `add` and `"*"` to `multiply` is cleaner and more extensible than branching for every operator. This pattern appears in web frameworks, CLI tools, and game engines.
182+
2. **`float()` and `int()` convert strings to numbers, but they crash on bad input.** Always wrap them in `try/except ValueError` when the data comes from a user or file. You will use this pattern in every project that reads numeric input.
183+
3. **Returning error dicts keeps the caller simple.** Instead of raising exceptions that force try/except everywhere, `calculate()` returns `{"error": "..."}` so the caller just checks `if "error" in result`. This is a beginner-friendly version of the "Result type" pattern used in many languages.
184+
4. **Functions that do one thing are easier to test.** `assert add(2, 3) == 5` is a one-line test. If `add` also handled parsing and printing, the test would need to set up input and capture output — much harder.

0 commit comments

Comments
 (0)