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.

WarningWARNING

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.

NoteTIP

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; True prints exactly with a capital T.
  • Exercise 2: input() always gives text. After wrapping it in int(...), 43 becomes a number and 43 x 7 = 301. (Without int, "43" * 7 only 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 :.2f rounds 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 True when 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 of and and or; 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 of int and float.
  • Exercise 2: 1000 // 30 gives 33 full trays (33 x 30 = 990 eggs) and 1000 % 30 gives the 10 leftover eggs.
  • Exercise 4: The expression is (diameter >= 40 and healthy) or height > 25. Python does and before or, 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 the height > 25 condition 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 < 25 and temp > 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 for and range().

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, using continue.

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 prints 7 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: continue skips 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, and ready_to_harvest (the result of a comparison). Print everything with for and .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_harvest is filled with the result of the comparison 540.5 >= 500, which is True. The loop for 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 returns None when the list is empty, so it doesn’t hit ZeroDivisionError. Use the if from 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 for None before 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 in dd-mm-yyyy format.

Exercise 2. With random.seed(7), choose 3 different pond numbers from 1 to 12 using random.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-2026 with strftime("%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 a float from the text, or None if the text is not a number. Test it with "12,5", "abc", and "".

Exercise 4. Challenge: make an error class UnreasonablePh(FieldDataError) and a function check_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 make float() raise ValueError, which is caught and turned into None. 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/except inside the loop: raise creates an error for one pond only, except UnreasonablePh catches 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. Because UnreasonablePh is a child of FieldDataError, 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 line date|kg to a file (mode "a"), and a function total_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 in int().
  • 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_harvest uses mode "a" so old records stay safe. total_harvest reads line by line, splits on |, and wraps float(...) in a try: a line without a separator triggers IndexError, and the value abc triggers ValueError. 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 livestock data to livestock.csv, read it back with DictReader, and print the total weight_kg (don’t forget to convert the types).

Exercise 2. Save the dict {"name": "Pond Ø1", "temp": "28 °C"} to JSON with and without ensure_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 why float(...) 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 \u00d8 and \u00b0; with False they are written as they are (Ø and °). Both are valid JSON and give the same data when read back; False is just easier for humans to read. The requirement is that the file is opened with encoding="utf-8".

Chapter 16 — OOP

Exercise 2. Make your own class Garden (not a child) with name and a list harvest_kg, then fill in __len__ and __str__. Print one object and call len() 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 by len(garden) (3 harvests) and __str__ by print(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-0042 in "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}\b means: two capital letters, a hyphen, four digits, with word boundaries at both ends. Only two results (JT-0042, MR-0107); xx-12 is 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 > 505 produces a true/false array (the mask) [False False True True True], and weights[mask] selects only the True ones: 510, 520, 530.
  • Exercise 2: count * avg_weight_g / 1000 is 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 to n. Print list(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 yield is a generator: it hands out one value, pauses, and carries on when asked again. list(...) exhausts it: [2, 4, 6, 8, 10]. For n = 1 no 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 by factor. Make times_three and times_ten, then try both.

Exercise 2. Write a decorator count_calls that stores the number of calls in a function attribute (wrapper.count). Apply it to one function, call it four times, and print count.

Exercise 4. Challenge: write a decorator at_least(limit) that raises ValueError if the function’s result is below limit. 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 remembers factor = 3 even though the outer function has finished. That is a closure. times_three(7) = 21 and times_ten(7) = 70.
  • Exercise 2: The wrapper increases the attribute count on itself on every call: four calls, count = 4. functools.wraps keeps __name__ as greet rather than wrapper.
  • Exercise 4: A decorator with arguments has three layers: at_least(limit) takes the argument and returns decorator(function), which returns wrapper. The wrapper checks the result: biomass_kg(800, 540.5) = 432.4 passes because it is above 100, while biomass_kg(10, 300) = 3.0 is below 100 so ValueError is raised with a message that names the function (thanks to wraps).

Chapter 32 — Context Managers

Exercise 1. Write a context manager class Recorder that prints “start” in __enter__ and “finish” in __exit__. Use it with with and add one print inside.

Exercise 4. Challenge: write a context manager log_step(name) using @contextmanager that 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 with body, finish. __exit__ always runs; returning False means an error (if any) is not swallowed.
  • Exercise 4: With @contextmanager, the code before yield is “enter” and the code after it is “exit”. The successful block prints start then finish (the else branch). The failing block prints start then FAILED (the except branch), after which raise throws the error again, and it is finally caught outside the with. Without raise, 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 | None that returns the position of name in the list, or None if absent.

Exercise 1 (Chapter 34). Make a model Livestock with fields barn: str, kind: Literal["cattle", "goat", "chicken"], age_months: int (must be >= 0), and weight_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_tree returns position 2 for “Mahogany” and None for the missing “Sengon”. The type int | None tells the caller it must check for None. This script does not run mypy, 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), and weight_kg (0 is not above 0). Pydantic reports them all in one ValidationError instead of stopping at the first. The error messages are in English because that is Pydantic’s default.