Appendix D — Answer Key for Selected Exercises
Nothing beats trying it yourself first. This appendix holds 40 selected exercises from 19 chapters (mostly Chapters 5 to 25, plus a few from Chapters 26 to 34 whose answers run quickly) with their answers. Every question is quoted exactly from its chapter, and every answer is a script that was really run; the output below it is the real output.
What you read here is one correct answer, not the only one. Yours may differ (variable names, order, or the way of calculating) as long as the result is the same. Exercises that say “change script … of the chapter” were not chosen here, because their answers depend on that script; the ones chosen can be solved on their own.
All scripts of this appendix are in the kode/ch74/ folder. On the web version, you can also download them all at once (zip). Questions that use input() are answered with typed input prepared in a .input file next to the script, so the output looks as if you typed it yourself.
How to use this appendix: (1) solve the exercise yourself; (2) run your solution; (3) only then compare it with the answer here. If the result is the same but the method differs, that is fine. If the result differs, read the explanation before looking at the code.
List of answers
| Chapter | Exercises answered | Script |
|---|---|---|
| 5 | Exercises 1, 2, 3 | 01_ch05.py |
| 6 | Exercises 1, 2, 4 | 02_ch06.py |
| 7 | Exercises 1, 3 | 03_ch07.py |
| 8 | Exercises 1, 2, 4 | 04_ch08.py |
| 9 | Exercises 1, 2, 3 | 05_ch09.py |
| 10 | Exercises 1, 2, 4 | 06_ch10.py |
| 11 | Exercises 1, 2 | 07_ch11.py |
| 12 | Exercises 1, 4 | 08_ch12.py |
| 13 | Exercises 1, 2, 4 | 09_ch13.py |
| 14 | Exercises 1, 2 | 10_ch14.py |
| 16 | Exercises 2 | 11_ch16.py |
| 22 | Exercises 1, 2 | 12_ch22.py |
| 23 | Exercises 1, 2 | 13_ch23.py |
| 26 | Exercises 4 | 14_ch26.py |
| 30 | Exercises 1 | 15_ch30.py |
| 31 | Exercises 1, 2, 4 | 16_ch31.py |
| 32 | Exercises 1, 4 | 17_ch32.py |
| 33 and 34 | Exercises 2 / 1 | 18_ch33_34.py |
In total 40 exercises from 19 chapters. Non-code exercises (for example “how many lines and why”) are answered with reasoning backed by the script output.
Chapter 5 — Variables and Data Types
Exercise 1. Create variables for one rice field: the rice variety (text), area in hectares (decimal), plant age in days (whole number), and whether it has been fertilised (true/false). Print all four with an f-string.
Exercise 2. Ask the user to type how many eggs were collected today, then print the total for a week (times 7). Don’t forget the
input()trap!
Exercise 3. Challenge: compute the area of a tree crown with a diameter of 6.4 metres (formula: 3.14 × r × r), and display it with two digits after the decimal point.
01_ch05.py
# Chapter 74 - Answer key: Chapter 5 exercises (variables and data types)
print("--- Exercise 1 ---")
variety = "Ciherang" # text (str)
area_ha = 0.75 # decimal (float)
age_days = 62 # whole number (int)
fertilised = True # true/false (bool)
print(f"Variety {variety}, area {area_ha} ha, age {age_days} days, fertilised: {fertilised}")
print("--- Exercise 2 ---")
eggs_today = int(input("Eggs today: ")) # input() always gives text, so convert to int
print(f"Eggs for a week: {eggs_today * 7}")
print("--- Exercise 3 (challenge) ---")
diameter_m = 6.4
radius = diameter_m / 2 # r = d / 2 = 3.2
crown_area = 3.14 * radius * radius
print(f"Crown area: {crown_area:.2f} m2")--- Exercise 1 ---
Variety Ciherang, area 0.75 ha, age 62 days, fertilised: True
--- Exercise 2 ---
Eggs today: 43
Eggs for a week: 301
--- Exercise 3 (challenge) ---
Crown area: 32.15 m2
Explanation.
- Exercise 1: Four values, four types:
str,float,int,bool. The f-string pastes them all into one sentence;Trueprints exactly with a capital T. - Exercise 2:
input()always gives text. After wrapping it inint(...), 43 becomes a number and 43 x 7 = 301. (Withoutint,"43" * 7only repeats the text seven times instead of calculating.) - Exercise 3: A 6.4 m diameter means a 3.2 m radius. 3.14 x 3.2 x 3.2 = 32.1536, and
:.2frounds only the display to 32.15 (the value in memory is unchanged).
Chapter 6 — Operators and Input/Output
Exercise 1. A farmer harvests 3 plantation blocks: 245.5 kg, 310 kg, and 198.25 kg. Calculate the total harvest and the average per block, then print them with an f-string.
Exercise 2. A barn holds 1,000 eggs to be packed in trays of 30. Using
//and%, work out how many full trays there are and how many eggs are left over.
Exercise 4. Challenge: write one expression that is
Truewhen a tree is fit to be felled: diameter at least 40 cm and healthy, or height over 25 m. Test it with several numbers. Be careful with the order ofandandor; use brackets.
02_ch06.py
# Chapter 74 - Answer key: Chapter 6 exercises (operators)
print("--- Exercise 1 ---")
blocks = [245.5, 310, 198.25]
total = sum(blocks)
average = total / len(blocks)
print(f"Total harvest: {total} kg, average per block: {average} kg")
print("--- Exercise 2 ---")
eggs, tray_size = 1000, 30
print(f"Full trays: {eggs // tray_size}, eggs left over: {eggs % tray_size}")
print("--- Exercise 4 (challenge) ---")
def fit_to_fell(diameter_cm, healthy, height_m):
return (diameter_cm >= 40 and healthy) or height_m > 25
tests = [(45, True, 20), (45, False, 20), (35, True, 20), (35, False, 27), (45, False, 27)]
for diameter, healthy, height in tests:
print(diameter, healthy, height, "->", fit_to_fell(diameter, healthy, height))--- Exercise 1 ---
Total harvest: 753.75 kg, average per block: 251.25 kg
--- Exercise 2 ---
Full trays: 33, eggs left over: 10
--- Exercise 4 (challenge) ---
45 True 20 -> True
45 False 20 -> False
35 True 20 -> False
35 False 27 -> True
45 False 27 -> True
Explanation.
- Exercise 1: Total 245.5 + 310 + 198.25 = 753.75 kg; average 753.75 / 3 = 251.25 kg. Note that
sum()accepts a mix ofintandfloat. - Exercise 2:
1000 // 30gives 33 full trays (33 x 30 = 990 eggs) and1000 % 30gives the 10 leftover eggs. - Exercise 4: The expression is
(diameter >= 40 and healthy) or height > 25. Python doesandbeforeor, but the brackets make the meaning readable. All five tests match the reasoning: a healthy 45 cm tree passes; an unhealthy, short 45 cm tree fails; a short 35 cm tree fails; and a 27 m tree passes even when unhealthy, because theheight > 25condition stands on its own.
Chapter 7 — Conditionals
Exercise 1. Write a program that asks for the pond water temperature (a decimal number) and prints “Too cold” if below 25, “Too hot” if above 32, and “Good temperature” otherwise. (The limits 25 and 32 are practice numbers.)
Exercise 3. Make a program that asks for a plant’s age in days and prints the stage: “Seedling” (0–14), “Vegetative” (15–45), “Flowering” (46–70), or “Harvest” (over 70). These are made-up numbers for practice.
03_ch07.py
# Chapter 74 - Answer key: Chapter 7 exercises (conditionals)
print("--- Exercise 1 ---")
def check_temperature():
temp = float(input("Pond water temperature (C): "))
if temp < 25:
print("Too cold")
elif temp > 32:
print("Too hot")
else:
print("Good temperature")
for _ in range(3): # run three times so all three branches show up
check_temperature()
print("--- Exercise 3 ---")
def decide_stage():
age = int(input("Plant age (days): "))
if age <= 14:
print("Seedling")
elif age <= 45:
print("Vegetative")
elif age <= 70:
print("Flowering")
else:
print("Harvest")
for _ in range(5): # five ages around the stage limits: 10, 14, 15, 70, 71
decide_stage()--- Exercise 1 ---
Pond water temperature (C): 22.5
Too cold
Pond water temperature (C): 28
Good temperature
Pond water temperature (C): 34
Too hot
--- Exercise 3 ---
Plant age (days): 10
Seedling
Plant age (days): 14
Seedling
Plant age (days): 15
Vegetative
Plant age (days): 70
Flowering
Plant age (days): 71
Harvest
Explanation.
- Exercise 1: Three calls touch the three branches: 22.5 (below 25) = Too cold, 28 = Good temperature, 34 (above 32) = Too hot. With the conditions
temp < 25andtemp > 32, exactly 25 and exactly 32 fall into “Good temperature” (this is reasoning from the code; neither was tested here). - Exercise 3: Five ages around the limits: 10 and 14 = Seedling, 15 = Vegetative, 70 = Flowering, 71 = Harvest. Ordering the
elifs from small to large lets each condition check only its upper limit. These stage numbers are made up for practice.
Chapter 8 — Loops
Exercise 1. Print the 7 times table (7 × 1 up to 7 × 10) with
forandrange().
Exercise 2. Given the cow weights
[350, 412.5, 389.0, 401.5], calculate the total and the average with the accumulator pattern.
Exercise 4. Challenge: from the list
[480, 0, 510, 530, 0, 495], count the valid readings (not 0) and the average of the valid weights, usingcontinue.
04_ch08.py
# Chapter 74 - Answer key: Chapter 8 exercises (loops)
print("--- Exercise 1 ---")
for i in range(1, 11):
print(f"7 x {i} = {7 * i}")
print("--- Exercise 2 ---")
cow_weights = [350, 412.5, 389.0, 401.5]
total = 0 # accumulator pattern: start from zero
for weight in cow_weights:
total += weight
print(f"Total {total} kg, average {total / len(cow_weights)} kg")
print("--- Exercise 4 (challenge) ---")
readings = [480, 0, 510, 530, 0, 495]
valid_count = 0
valid_total = 0
for weight in readings:
if weight == 0:
continue # skip invalid readings
valid_count += 1
valid_total += weight
print(f"Valid: {valid_count} of {len(readings)}, average {valid_total / valid_count} g")--- Exercise 1 ---
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
7 x 10 = 70
--- Exercise 2 ---
Total 1553.0 kg, average 388.25 kg
--- Exercise 4 (challenge) ---
Valid: 4 of 6, average 503.75 g
Explanation.
- Exercise 1:
range(1, 11)yields 1 to 10 (11 is excluded). Each round prints7 x i = result. - Exercise 2: The accumulator pattern: start at 0 and add one by one. Total 350 + 412.5 + 389 + 401.5 = 1553.0 kg, average 1553.0 / 4 = 388.25 kg.
- Exercise 4:
continueskips the rest of the loop body for a reading of 0, so only 4 of the 6 readings are counted: (480 + 510 + 530 + 495) / 4 = 503.75 g. If the zeros were counted, the average would drop to about 335.8 g (2015 / 6), far from reality.
Chapter 9 — Built-in Data Structures
Exercise 1. Make a list with the weights of five cows (any numbers). Print the heaviest, the lightest, and the average.
Exercise 2. From the list
[480, 500, 510, 520, 530, 540], use slicing to take: the last three, and the first three reversed.
Exercise 3. Make a dict for one pond with the labels
name,fish_count,avg_weight_g, andready_to_harvest(the result of a comparison). Print everything withforand.items().
05_ch09.py
# Chapter 74 - Answer key: Chapter 9 exercises (data structures)
print("--- Exercise 1 ---")
weights = [350, 412.5, 389.0, 401.5, 365.0]
print("Heaviest:", max(weights), "| lightest:", min(weights), "| average:", sum(weights) / len(weights))
print("--- Exercise 2 ---")
numbers = [480, 500, 510, 520, 530, 540]
print("Last three:", numbers[-3:])
print("First three reversed:", numbers[:3][::-1])
print("--- Exercise 3 ---")
pond = {"name": "A", "fish_count": 800, "avg_weight_g": 540.5}
pond["ready_to_harvest"] = pond["avg_weight_g"] >= 500 # result of a comparison: True/False
for label, value in pond.items():
print(f"{label}: {value}")--- Exercise 1 ---
Heaviest: 412.5 | lightest: 350 | average: 383.6
--- Exercise 2 ---
Last three: [520, 530, 540]
First three reversed: [510, 500, 480]
--- Exercise 3 ---
name: A
fish_count: 800
avg_weight_g: 540.5
ready_to_harvest: True
Explanation.
- Exercise 1: Five example weights (350, 412.5, 389, 401.5, 365): heaviest 412.5, lightest 350, average 1918 / 5 = 383.6 kg.
- Exercise 2:
numbers[-3:]takes three from the back: [520, 530, 540].numbers[:3][::-1]takes the first three [480, 500, 510] and then reverses them: [510, 500, 480]. - Exercise 3: The label
ready_to_harvestis filled with the result of the comparison540.5 >= 500, which isTrue. The loopfor label, value in pond.items()prints every key and value pair.
Chapter 10 — Functions
Exercise 1. Write a function
circle_area_m2(radius_m)that returns the area (use 3.14 for π). Call it for radius 3 and 5.
Exercise 2. Write a function
harvest_status(weight_g)that returns"ready"if the weight is ≥ 500 and"not yet"otherwise. Use it inside a loop over the list[480, 520, 505, 495].
Exercise 4. Challenge: write a function
safe_average(numbers)that returnsNonewhen the list is empty, so it doesn’t hitZeroDivisionError. Use theiffrom Chapter 7.
06_ch10.py
# Chapter 74 - Answer key: Chapter 10 exercises (functions)
print("--- Exercise 1 ---")
def circle_area_m2(radius_m):
return round(3.14 * radius_m * radius_m, 2) # round() avoids 28.259999999999998
print(circle_area_m2(3), circle_area_m2(5))
print("--- Exercise 2 ---")
def harvest_status(weight_g):
if weight_g >= 500:
return "ready"
return "not yet"
for weight in [480, 520, 505, 495]:
print(weight, "->", harvest_status(weight))
print("--- Exercise 4 (challenge) ---")
def safe_average(numbers):
if len(numbers) == 0:
return None
return sum(numbers) / len(numbers)
print(safe_average([480, 520]), safe_average([]))--- Exercise 1 ---
28.26 78.5
--- Exercise 2 ---
480 -> not yet
520 -> ready
505 -> ready
495 -> not yet
--- Exercise 4 (challenge) ---
500.0 None
Explanation.
- Exercise 1: Radius 3 gives 28.26 and radius 5 gives 78.5. Without
round(..., 2), Python prints 28.259999999999998 because decimals are stored as binary approximations; rounding keeps the output tidy. - Exercise 2: The function returns text, so the caller can use it freely: 480 and 495 are not ready, 520 and 505 are ready (the limit is
>= 500). - Exercise 4: The empty list is guarded by
if len(numbers) == 0: return None, so there is no division by zero. The caller must check forNonebefore using the result in a calculation.
Chapter 11 — Modules and Packages
Exercise 1. Use
datetime: work out the harvest date for coffee planted on 15 January 2026 and harvested 270 days later, then print it indd-mm-yyyyformat.
Exercise 2. With
random.seed(7), choose 3 different pond numbers from 1 to 12 usingrandom.sample. Run it twice and make sure the result is the same.
07_ch11.py
# Chapter 74 - Answer key: Chapter 11 exercises (modules)
import random
from datetime import date, timedelta
print("--- Exercise 1 ---")
planted = date(2026, 1, 15)
harvest = planted + timedelta(days=270)
print("Coffee harvest date:", harvest.strftime("%d-%m-%Y"))
print("--- Exercise 2 ---")
for round_no in (1, 2):
random.seed(7) # same seed -> same random sequence
print(f"Round {round_no}:", random.sample(range(1, 13), 3))--- Exercise 1 ---
Coffee harvest date: 12-10-2026
--- Exercise 2 ---
Round 1: [6, 3, 7]
Round 2: [6, 3, 7]
Explanation.
- Exercise 1: 15 January 2026 plus 270 days lands on 12 October 2026, printed as
12-10-2026withstrftime("%d-%m-%Y"). - Exercise 2: Each
random.seed(7)returns the random generator to its starting state, so both rounds give [6, 3, 7]. The exact numbers depend on the Python version (3.14 here); what always holds is that both rounds match.
Chapter 12 — Error Handling
Exercise 1. Write a function
to_number(text)that returns afloatfrom the text, orNoneif the text is not a number. Test it with"12,5","abc", and"".
Exercise 4. Challenge: make an error class
UnreasonablePh(FieldDataError)and a functioncheck_ponds(items)that collects all problem ponds (instead of stopping at the first), then prints a summary.
08_ch12.py
# Chapter 74 - Answer key: Chapter 12 exercises (errors)
print("--- Exercise 1 ---")
def to_number(text):
try:
return float(text.replace(",", ".")) # decimal comma, as the exercise writes it
except ValueError:
return None
for text in ["12,5", "abc", ""]:
print(repr(text), "->", to_number(text))
print("--- Exercise 4 (challenge) ---")
class FieldDataError(Exception):
"""Parent of all field data errors."""
class UnreasonablePh(FieldDataError):
pass
def check_ponds(items):
problems = []
for pond in items:
try:
if not 0 <= pond["water_ph"] <= 14:
raise UnreasonablePh(f"Pond {pond['pond']}: pH {pond['water_ph']} is impossible")
if not 6.5 <= pond["water_ph"] <= 8.5:
raise UnreasonablePh(f"Pond {pond['pond']}: pH {pond['water_ph']} outside 6.5-8.5")
except UnreasonablePh as e:
problems.append(str(e)) # collect it, do not stop
return problems
test_ponds = [
{"pond": "A", "water_ph": 7.2},
{"pond": "B", "water_ph": 5.1},
{"pond": "C", "water_ph": 19.0},
{"pond": "D", "water_ph": 8.9},
]
result = check_ponds(test_ponds)
print(f"{len(result)} of {len(test_ponds)} ponds have problems:")
for line in result:
print(" -", line)--- Exercise 1 ---
'12,5' -> 12.5
'abc' -> None
'' -> None
--- Exercise 4 (challenge) ---
3 of 4 ponds have problems:
- Pond B: pH 5.1 outside 6.5-8.5
- Pond C: pH 19.0 is impossible
- Pond D: pH 8.9 outside 6.5-8.5
Explanation.
- Exercise 1:
float()rejects a comma, so the function first swaps the comma for a dot: “12,5” becomes 12.5. “abc” and empty text makefloat()raiseValueError, which is caught and turned intoNone. Swapping the comma is my choice to match how decimals are written in Indonesian; another valid choice is to reject the comma. - Exercise 4: The challenge is to collect every problem instead of stopping at the first one. The key is a
try/exceptinside the loop:raisecreates an error for one pond only,except UnreasonablePhcatches it, the message goes into the list and the loop carries on. The result is 3 of 4 ponds with problems (B pH 5.1; C pH 19.0, impossible because the pH scale only runs from 0 to 14; D pH 8.9), while A (7.2) is fine. The 6.5 to 8.5 limit is a practice number. BecauseUnreasonablePhis a child ofFieldDataError, you could later catch every kind of field data error at once through the parent.
Chapter 13 — Files and Basic I/O
Exercise 1. Write a script that writes the numbers 1 to 10 (one per line) to
numbers.txt, then reads it back and prints the sum.
Exercise 2. Run the script from exercise 1 twice with mode
"a"(not"w"). How many lines does the file hold, and why?
Exercise 4. Challenge: write a function
log_harvest(file, date, kg)that appends one linedate|kgto a file (mode"a"), and a functiontotal_harvest(file)that adds up the kg column while skipping broken lines.
09_ch13.py
# Chapter 74 - Answer key: Chapter 13 exercises (files)
from pathlib import Path
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
print("--- Exercise 1 ---")
file = tmp / "numbers.txt"
with open(file, "w", encoding="utf-8") as f:
for n in range(1, 11):
f.write(f"{n}\n")
with open(file, encoding="utf-8") as f:
total = sum(int(line) for line in f)
print("Sum:", total)
print("--- Exercise 2 ---")
file_a = tmp / "numbers_append.txt"
file_a.unlink(missing_ok=True) # start from a file that does not exist
for round_no in (1, 2): # same as running the script twice
with open(file_a, "a", encoding="utf-8") as f:
for n in range(1, 11):
f.write(f"{n}\n")
print(f"After round {round_no}: {len(file_a.read_text(encoding='utf-8').splitlines())} lines")
print("--- Exercise 4 (challenge) ---")
def log_harvest(file, date, kg):
with open(file, "a", encoding="utf-8") as f:
f.write(f"{date}|{kg}\n")
def total_harvest(file):
total = 0.0
for line in Path(file).read_text(encoding="utf-8").splitlines():
parts = line.split("|")
try:
total += float(parts[1])
except (IndexError, ValueError):
print(" broken line skipped:", repr(line))
return total
harvest = tmp / "harvest.txt"
harvest.unlink(missing_ok=True)
log_harvest(harvest, "2026-10-01", 245.5)
log_harvest(harvest, "2026-10-02", 310)
with open(harvest, "a", encoding="utf-8") as f:
f.write("broken line without separator\n")
f.write("2026-10-03|abc\n")
log_harvest(harvest, "2026-10-04", 198.25)
print("Total harvest:", total_harvest(harvest), "kg")--- Exercise 1 ---
Sum: 55
--- Exercise 2 ---
After round 1: 10 lines
After round 2: 20 lines
--- Exercise 4 (challenge) ---
broken line skipped: 'broken line without separator'
broken line skipped: '2026-10-03|abc'
Total harvest: 753.75 kg
Explanation.
- Exercise 1: One number per line (
"1\n","2\n", …), read back and added up: 1 + 2 + … + 10 = 55. Each line read is still text, so it is wrapped inint(). - Exercise 2: It holds 20 lines: mode
"a"(append) adds at the end of the file without deleting what was there, so every run piles up 10 new lines (10 after the first round, 20 after the second). Mode"w"overwrites and would always leave 10 lines. In the script the second run is represented by a loop; the result equals running the script twice from a file that does not exist yet. - Exercise 4:
log_harvestuses mode"a"so old records stay safe.total_harvestreads line by line, splits on|, and wrapsfloat(...)in atry: a line without a separator triggersIndexError, and the valueabctriggersValueError. Both are caught, the broken lines are reported, and the calculation carries on. The total is 245.5 + 310 + 198.25 = 753.75 kg from the three healthy lines.
Chapter 14 — Data Formats
Exercise 1. Write the
livestockdata tolivestock.csv, read it back withDictReader, and print the totalweight_kg(don’t forget to convert the types).
Exercise 2. Save the dict
{"name": "Pond Ø1", "temp": "28 °C"}to JSON with and withoutensure_ascii=False. Compare the file contents.
10_ch14.py
# Chapter 74 - Answer key: Chapter 14 exercises (CSV and JSON)
import csv
import json
from pathlib import Path
tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)
print("--- Exercise 1 ---")
livestock = [
{"barn": "K1", "kind": "cattle", "age_months": 36, "weight_kg": 350.0},
{"barn": "K1", "kind": "cattle", "age_months": 30, "weight_kg": 412.5},
{"barn": "K2", "kind": "cattle", "age_months": 42, "weight_kg": 389.0},
{"barn": "K3", "kind": "goat", "age_months": 14, "weight_kg": 38.5},
]
with open(tmp / "livestock.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(livestock[0]))
writer.writeheader()
writer.writerows(livestock)
with open(tmp / "livestock.csv", newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
print("First value was read as:", repr(rows[0]["weight_kg"]), "(text!)")
print("Total weight_kg:", sum(float(r["weight_kg"]) for r in rows))
print("--- Exercise 2 ---")
data = {"name": "Pond Ø1", "temp": "28 °C"}
for ascii_only in (True, False):
file = tmp / f"pond_ascii_{ascii_only}.json"
with open(file, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=ascii_only)
print(f"ensure_ascii={ascii_only}:", file.read_text(encoding="utf-8"))--- Exercise 1 ---
First value was read as: '350.0' (text!)
Total weight_kg: 1190.0
--- Exercise 2 ---
ensure_ascii=True: {"name": "Pond \u00d81", "temp": "28 \u00b0C"}
ensure_ascii=False: {"name": "Pond Ø1", "temp": "28 °C"}
Explanation.
- Exercise 1: The first value is read as
'350.0', which is text: CSV has no types. That is whyfloat(...)is required before adding. Total 350 + 412.5 + 389 + 38.5 = 1190.0 kg. - Exercise 2: With
ensure_ascii=True(the default), non-ASCII characters are written as the codes\u00d8and\u00b0; withFalsethey are written as they are (Ø and °). Both are valid JSON and give the same data when read back;Falseis just easier for humans to read. The requirement is that the file is opened withencoding="utf-8".
Chapter 16 — OOP
Exercise 2. Make your own class
Garden(not a child) withnameand a listharvest_kg, then fill in__len__and__str__. Print one object and calllen()on it.
11_ch16.py
# Chapter 74 - Answer key: Chapter 16 exercises (OOP)
class Garden:
"""A garden of its own (not a child of another class)."""
def __init__(self, name, harvest_kg):
self.name = name
self.harvest_kg = harvest_kg
def __len__(self): # called by len(object)
return len(self.harvest_kg)
def __str__(self): # called by print(object)
return f"Garden {self.name}: {len(self)} harvests, total {sum(self.harvest_kg)} kg"
garden = Garden("North Coffee", [245.5, 310.0, 198.5])
print(garden)
print("len:", len(garden))Garden North Coffee: 3 harvests, total 754.0 kg
len: 3
Explanation.
- Exercise 2:
__len__is called bylen(garden)(3 harvests) and__str__byprint(garden). Inside__str__,len(self)reuses__len__. Total 245.5 + 310 + 198.5 = 754.0 kg.
Chapter 22 — Regex and Text Processing
Exercise 1. Write a pattern that finds every code like
JT-0042in"JT-0042, MR-0107, xx-12". (Only two results.)
Exercise 2. With
re.sub, turn"Plot A,Plot B , Plot C"into"Plot A, Plot B, Plot C".
12_ch22.py
# Chapter 74 - Answer key: Chapter 22 exercises (regex)
import re
print("--- Exercise 1 ---")
text = "JT-0042, MR-0107, xx-12"
pattern = r"\b[A-Z]{2}-\d{4}\b" # two capital letters, a hyphen, four digits
print(re.findall(pattern, text))
print("--- Exercise 2 ---")
messy = "Plot A,Plot B , Plot C"
step1 = re.sub(r"\s*,\s*", ", ", messy) # commas: no space before, one space after
clean = re.sub(r"\s{2,}", " ", step1) # double spaces become one
print(repr(step1))
print(repr(clean))--- Exercise 1 ---
['JT-0042', 'MR-0107']
--- Exercise 2 ---
'Plot A, Plot B, Plot C'
'Plot A, Plot B, Plot C'
Explanation.
- Exercise 1: The pattern
\b[A-Z]{2}-\d{4}\bmeans: two capital letters, a hyphen, four digits, with word boundaries at both ends. Only two results (JT-0042,MR-0107);xx-12is rejected because its letters are lowercase and it has only two digits. - Exercise 2: Two steps. First
\s*,\s*replaces a comma and the spaces around it with", "(exactly one space after the comma). Second\s{2,}turns double spaces into one. The final result is exactly what was asked.
Chapter 23 — Introduction to numpy and pandas
Exercise 1. From the array
np.array([480, 500, 510, 520, 530]), compute the mean weight and select those heavier than 505 with a mask.
Exercise 2. Make a fish DataFrame with the columns
pond,species,count,avg_weight_g, then compute the biomass of each row (count * avg_weight_g / 1000) as a new column.
13_ch23.py
# Chapter 74 - Answer key: Chapter 23 exercises (numpy and pandas)
import numpy as np
import pandas as pd
print("--- Exercise 1 ---")
weights = np.array([480, 500, 510, 520, 530])
print("Mean:", weights.mean())
mask = weights > 505 # array of True/False
print("Mask:", mask)
print("Above 505:", weights[mask])
print("--- Exercise 2 ---")
fish = pd.DataFrame({
"pond": ["A", "B", "C"],
"species": ["catfish", "tilapia", "catfish"],
"count": [800, 1250, 600],
"avg_weight_g": [540.5, 300.0, 480.0],
})
fish["biomass_kg"] = fish["count"] * fish["avg_weight_g"] / 1000
print(fish)--- Exercise 1 ---
Mean: 508.0
Mask: [False False True True True]
Above 505: [510 520 530]
--- Exercise 2 ---
pond species count avg_weight_g biomass_kg
0 A catfish 800 540.5 432.4
1 B tilapia 1250 300.0 375.0
2 C catfish 600 480.0 288.0
Explanation.
- Exercise 1: The mean is 508.0.
weights > 505produces a true/false array (the mask)[False False True True True], andweights[mask]selects only theTrueones: 510, 520, 530. - Exercise 2:
count * avg_weight_g / 1000is calculated for every row at once (no loop): 800 x 540.5 / 1000 = 432.4; 1250 x 300 / 1000 = 375.0; 600 x 480 / 1000 = 288.0 kg.
Chapter 26 — Statistics with Python
Exercise 4. Challenge: create data for two plots with exactly the same mean, repeat the t-test 1000 times with different seeds, and count what percentage of tests give p below 0.05. Is it close to 5%? What does it mean?
14_ch26.py
# Chapter 74 - Answer key: Chapter 26 exercises (statistics), challenge
import numpy as np
from scipy import stats
repeats = 1000
small_p = 0
for seed in range(repeats):
rng = np.random.default_rng(seed) # a different seed each repeat
plot_1 = rng.normal(loc=30, scale=5, size=30) # two plots, the SAME population mean (30)
plot_2 = rng.normal(loc=30, scale=5, size=30)
p = stats.ttest_ind(plot_1, plot_2).pvalue
if p < 0.05:
small_p += 1
print(f"Tests with p < 0.05: {small_p} of {repeats} ({small_p / repeats * 100:.1f}%)")Tests with p < 0.05: 56 of 1000 (5.6%)
Explanation.
- Exercise 4: Both plots are drawn from exactly the same population (mean 30, standard deviation 5), so there is no real difference. Even so, the t-test says “different” (p below 0.05) in 56 of 1000 repeats, that is 5.6%, very close to 5%. It means: the 0.05 limit really says “I accept being wrong about 5% of the time”; among tests on data that do not differ, about 1 in 20 still looks “real”. That is why one small p alone proves nothing if you test many things. The number 56 comes from seeds 0 to 999; other seeds give a slightly different number around 5% (a statistical expectation, not tested here).
Chapter 30 — Iterators and Generators
Exercise 1. Write a generator function
evens_up_to(n)that yields the even numbers from 2 ton. Printlist(evens_up_to(10)).
15_ch30.py
# Chapter 74 - Answer key: Chapter 30 exercises (generators)
def evens_up_to(n):
number = 2
while number <= n:
yield number # hands out one value, then pauses
number += 2
print(list(evens_up_to(10)))
print(list(evens_up_to(1))) # limit too small: empty[2, 4, 6, 8, 10]
[]
Explanation.
- Exercise 1: A function with
yieldis a generator: it hands out one value, pauses, and carries on when asked again.list(...)exhausts it: [2, 4, 6, 8, 10]. Forn = 1no even number qualifies, so the result is an empty list, not an error.
Chapter 31 — Decorators and Closures
Exercise 1. Write a closure
make_multiplier(factor)that returns a function multiplying a number byfactor. Maketimes_threeandtimes_ten, then try both.
Exercise 2. Write a decorator
count_callsthat stores the number of calls in a function attribute (wrapper.count). Apply it to one function, call it four times, and printcount.
Exercise 4. Challenge: write a decorator
at_least(limit)that raisesValueErrorif the function’s result is belowlimit. Apply it to the biomass function, and test with one input that passes and one that fails.
16_ch31.py
# Chapter 74 - Answer key: Chapter 31 exercises (closures and decorators)
import functools
print("--- Exercise 1 ---")
def make_multiplier(factor):
def multiply(number):
return number * factor # factor is "remembered" from the outer function
return multiply
times_three = make_multiplier(3)
times_ten = make_multiplier(10)
print(times_three(7), times_ten(7))
print("--- Exercise 2 ---")
def count_calls(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
wrapper.count += 1
return function(*args, **kwargs)
wrapper.count = 0
return wrapper
@count_calls
def greet(name):
return f"Hello, {name}"
for name in ["Budi", "Sari", "Joko", "Tini"]:
greet(name)
print("Number of calls:", greet.count, "| function name:", greet.__name__)
print("--- Exercise 4 (challenge) ---")
def at_least(limit):
def decorator(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
result = function(*args, **kwargs)
if result < limit:
raise ValueError(f"{function.__name__} returned {result}, below the limit {limit}")
return result
return wrapper
return decorator
@at_least(100)
def biomass_kg(count, weight_g):
return count * weight_g / 1000
print(biomass_kg(800, 540.5))
try:
print(biomass_kg(10, 300))
except ValueError as e:
print("ValueError:", e)--- Exercise 1 ---
21 70
--- Exercise 2 ---
Number of calls: 4 | function name: greet
--- Exercise 4 (challenge) ---
432.4
ValueError: biomass_kg returned 3.0, below the limit 100
Explanation.
- Exercise 1:
make_multiplier(3)returns a function that remembersfactor = 3even though the outer function has finished. That is a closure.times_three(7)= 21 andtimes_ten(7)= 70. - Exercise 2: The wrapper increases the attribute
counton itself on every call: four calls,count= 4.functools.wrapskeeps__name__asgreetrather thanwrapper. - Exercise 4: A decorator with arguments has three layers:
at_least(limit)takes the argument and returnsdecorator(function), which returnswrapper. The wrapper checks the result:biomass_kg(800, 540.5)= 432.4 passes because it is above 100, whilebiomass_kg(10, 300)= 3.0 is below 100 soValueErroris raised with a message that names the function (thanks towraps).
Chapter 32 — Context Managers
Exercise 1. Write a context manager class
Recorderthat prints “start” in__enter__and “finish” in__exit__. Use it withwithand add one
Exercise 4. Challenge: write a context manager
log_step(name)using@contextmanagerthat prints “[name] start” and “[name] finish”; if an error occurs, print “[name] FAILED” and then raise the error again. Test it with one successful block and one failing block.
17_ch32.py
# Chapter 74 - Answer key: Chapter 32 exercises (context managers)
from contextlib import contextmanager
print("--- Exercise 1 ---")
class Recorder:
def __enter__(self):
print("start")
return self
def __exit__(self, error_type, value, trace):
print("finish")
return False # do not swallow errors
with Recorder():
print(" working inside with")
print("--- Exercise 4 (challenge) ---")
@contextmanager
def log_step(name):
print(f"[{name}] start")
try:
yield
except Exception:
print(f"[{name}] FAILED")
raise # raise the error again
else:
print(f"[{name}] finish")
with log_step("success"):
print(" inside the successful block")
try:
with log_step("failure"):
raise ValueError("broken data")
except ValueError as e:
print("error caught outside:", e)--- Exercise 1 ---
start
working inside with
finish
--- Exercise 4 (challenge) ---
[success] start
inside the successful block
[success] finish
[failure] start
[failure] FAILED
error caught outside: broken data
Explanation.
- Exercise 1: The print order: start, the
withbody, finish.__exit__always runs; returningFalsemeans an error (if any) is not swallowed. - Exercise 4: With
@contextmanager, the code beforeyieldis “enter” and the code after it is “exit”. The successful block prints start then finish (theelsebranch). The failing block prints start then FAILED (theexceptbranch), after whichraisethrows the error again, and it is finally caught outside thewith. Withoutraise, the error would vanish silently.
Chapter 33 and 34 — Type Hints and Data Validation and Configuration
Exercise 2 (Chapter 33). Write a function
find_tree(items: list[str], name: str) -> int | Nonethat returns the position ofnamein the list, orNoneif absent.
Exercise 1 (Chapter 34). Make a model
Livestockwith fieldsbarn: str,kind: Literal["cattle", "goat", "chicken"],age_months: int(must be>= 0), andweight_kg: float(must be> 0). Try one correct record and one wrong one.
18_ch33_34.py
# Chapter 74 - Answer key: Chapter 33 (type hints) and Chapter 34 (pydantic) exercises
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
print("--- Chapter 33, exercise 2 ---")
def find_tree(items: list[str], name: str) -> int | None:
if name in items:
return items.index(name)
return None
trees = ["Teak", "Meranti", "Mahogany"]
print(find_tree(trees, "Mahogany"), find_tree(trees, "Sengon"))
print("--- Chapter 34, exercise 1 ---")
class Livestock(BaseModel):
barn: str
kind: Literal["cattle", "goat", "chicken"]
age_months: int = Field(ge=0)
weight_kg: float = Field(gt=0)
print(Livestock(barn="K1", kind="cattle", age_months=36, weight_kg=350.0))
try:
Livestock(barn="K2", kind="duck", age_months=-3, weight_kg=0)
except ValidationError as e:
print(f"{e.error_count()} errors:")
for error in e.errors():
print(" -", error["loc"][0], "->", error["msg"])--- Chapter 33, exercise 2 ---
2 None
--- Chapter 34, exercise 1 ---
barn='K1' kind='cattle' age_months=36 weight_kg=350.0
3 errors:
- kind -> Input should be 'cattle', 'goat' or 'chicken'
- age_months -> Input should be greater than or equal to 0
- weight_kg -> Input should be greater than 0
Explanation.
- Chapter 33, exercise 2:
find_treereturns position 2 for “Mahogany” andNonefor the missing “Sengon”. The typeint | Nonetells the caller it must check forNone. This script does not runmypy, so the type check itself is not demonstrated here. - Chapter 34, exercise 1: The correct record passes and prints neatly. The wrong record triggers three errors at once:
kind(“duck” is not one of the choices),age_months(-3 is below 0), andweight_kg(0 is not above 0). Pydantic reports them all in oneValidationErrorinstead of stopping at the first. The error messages are in English because that is Pydantic’s default.