Separate exercise solutions
Compare these references after your own attempt. You can read them at any time; they do not record a live model or vendor run.
Try the exercises first. These are reference approaches, not the only acceptable code. For Modules 0–3 the answers deliberately use simple statements; later modules introduce reusable functions. Tests cover these reference answers, not unedited learner starters.
Module 0
The code is the saved instruction text; the printed greeting and 35 are output. A comment explains intent and is not executed. Without an assignment, a reference to minutes raises NameError.
# Reference answer. Attempt the exercise first.
# 0A
print("Hello, Aabha learner!")
# 0B and 0C: 20 minutes plus a 15-minute session totals 35.
minutes = 20
print(minutes + 15)
Exit-check guide: The downloaded file check is a learner action, not something the reference program can verify.
Module 1
Integer multiplication gives 700 cents. Floor division and remainder give 2 hours and 15 minutes. Conversion changes the kind of value; strip/lower changes text content.
# Reference answer. Attempt the exercise first.
quantity = 4
unit_price_cents = 175
total = quantity * unit_price_cents
print(total, "cents")
hours = 135 // 60
minutes_left = 135 % 60
raw = "7"
topic = " Data "
result = int(raw) + 3
normalized = topic.strip().lower()
assert total == 700
assert (hours, minutes_left) == (2, 15)
assert result == 10 and normalized == "data"
print("Module 1 checks passed")
Exit-check guide: The values "20", 20 and 20.0 have different types. int(2.9) discards the fractional part; it does not validate an integer count. The precedence answers are 14 and 20.
Module 2
Check negative and zero before the short-session branch so categories do not overlap. Initialize the accumulator outside the loop. The countdown decreases until its condition is false.
# Reference answer. Attempt the exercise first.
minutes = 30
if minutes < 0:
label = "invalid"
elif minutes == 0:
label = "rest"
elif minutes < 30:
label = "short"
else:
label = "long"
total = 0
for value in [10, -5, 0, 25]:
if value > 0:
total += value
remaining = 3
countdown = []
while remaining > 0:
countdown.append(remaining)
remaining -= 1
assert label == "long" and total == 35 and countdown == [3, 2, 1]
print("Module 2 checks passed")
Exit-check guide: The trace totals are 10, 10, 10, 35. Empty and all-negative inputs total zero. range(2,5) ends at 4. The input 30 distinguishes > from >=.
Module 3
get(topic,0) supplies the initial count; each occurrence adds one. copy creates the independent outer list. Sorting a set makes the unique output order explicit.
# Reference answer. Attempt the exercise first.
topics = ["python", "data", "python", "charts", "data"]
counts = {}
for topic in topics:
counts[topic] = counts.get(topic, 0) + 1
original = [20, 30]
edited = original.copy()
edited.append(40)
unique = sorted(set(topics))
assert counts == {"python": 2, "data": 2, "charts": 1}
assert original == [20, 30] and edited == [20, 30, 40]
assert unique == ["charts", "data", "python"]
print("Module 3 checks passed")
Exit-check guide: Dictionary keys name fields; sequence indices locate positions. A zero default can hide a missing required measurement. A tuple may contain a mutable list, and a shallow copy can still share nested objects.
Module 4
A returned result can feed the next calculation. The two sessions total 60 minutes, which converts to 1.0 hour. Empty summaries have zero count and total, while negative duration violates the conversion contract.
# Reference answer. Attempt the exercise first.
def minutes_to_hours(minutes):
if minutes < 0:
raise ValueError("minutes must be non-negative")
return minutes / 60
def summarize_minutes(values):
return {"count": len(values), "total": sum(values)}
values = [15, 45]
summary = summarize_minutes(values)
print(summary)
print(minutes_to_hours(summary["total"]))
assert summarize_minutes([]) == {"count": 0, "total": 0}
assert minutes_to_hours(0) == 0
try:
minutes_to_hours(-1)
except ValueError:
print("Negative-input check passed")
else:
raise AssertionError("Expected ValueError")
Exit-check guide: A parameter is a name in the definition; an argument is supplied by a call. Printing presents information, while return provides a result to the caller. Explicit parameters remove hidden global dependencies.
Module 5
The conversion rejects malformed and negative values. The original text and record position remain in the reject list. DictWriter quotes a topic containing a comma, so the reader reconstructs it correctly.
# Reference answer. Attempt the exercise first.
import csv
from io import StringIO
def parse_minutes(raw):
value = int(raw)
if value < 0:
raise ValueError("minutes must be non-negative")
return value
def read_sessions(csv_text):
accepted, rejected = [], []
with StringIO(csv_text) as source:
for line_number, row in enumerate(csv.DictReader(source), start=2):
try:
value = parse_minutes(row["minutes"])
except ValueError:
rejected.append({"line": line_number, "raw": row["minutes"]})
else:
accepted.append({"topic": row["topic"], "minutes": value})
return accepted, rejected
def encode_summary(summary):
output = StringIO()
writer = csv.DictWriter(output, fieldnames=["topic", "minutes"])
writer.writeheader()
writer.writerows(summary)
return output.getvalue()
text = "topic,minutes\npython,20\ndata,unknown\ncharts,-1\npython,0\n"
accepted, rejected = read_sessions(text)
assert len(accepted) == 2 and len(rejected) == 2
assert sum(row["minutes"] for row in accepted) == 20
print(encode_summary(accepted))
quoted = encode_summary([{"topic": "data, charts", "minutes": 10}])
assert list(csv.DictReader(StringIO(quoted)))[0]["topic"] == "data, charts"
print("CSV round-trip check passed")
Exit-check guide: A value can parse successfully and still violate the task rule. Missing minutes are unknown, not zero. A missing required header should remain a visible error. The with block closes the source on normal or exceptional exit.
Module 6
The accumulator belongs before the loop. Empty input now returns an active count of zero; the mean still rejects empty input because no average exists. Approximate comparisons pass the decimal representation check but reject a real difference of 0.01.
# Reference answer. Attempt the exercise first.
import math
def mean_minutes(values):
if not values:
raise ValueError("at least one value is required")
return sum(values) / len(values)
def count_active(values):
count = 0 # Keep the accumulator outside the loop.
for value in values:
if value > 0:
count += 1
return count
for values, expected in [([10, 0, 20], 2), ([], 0), ([0], 0), ([7], 1)]:
assert count_active(values) == expected
for values, expected in [([10,20,30],20), ([7],7), ([0,0],0)]:
assert math.isclose(mean_minutes(values), expected)
try:
mean_minutes([])
except ValueError:
pass
else:
raise AssertionError("Expected ValueError")
assert math.isclose(0.1 + 0.2, 0.3, rel_tol=1e-9, abs_tol=1e-12)
assert not math.isclose(0.31, 0.3, rel_tol=1e-9, abs_tol=1e-12)
print("Module 6 checks passed")
Exit-check guide: Two positive values expose the reset bug; an empty list exposes the uninitialized name. A regression test keeps the earlier failure reproducible. Passing cases do not prove every input, device or teaching outcome.
Module 7
axis=1 reduces each row to one day total; axis=0 reduces rows to one total per column. A mask selects matching elements, and a high threshold produces an empty one-dimensional result. copy isolates the edit.
# Reference answer. Attempt the exercise first.
import numpy as np
values = np.array([[10., 40.], [20., 50.]])
row_totals = values.sum(axis=1)
column_totals = values.sum(axis=0)
selected = values[values >= 40]
first = values[0].copy()
first[0] = 99
assert np.array_equal(row_totals, [50, 70])
assert np.array_equal(column_totals, [30, 90])
assert np.array_equal(selected, [40, 50])
assert first[0] == 99 and values[0, 0] == 10
assert values[values >= 99].shape == (0,)
print("Module 7 checks passed")
# Transfer checkpoint: non-square data and approximate division.
transfer = np.array([[15, 30, 45], [20, 40, 60]])
assert transfer.sum(axis=0).shape == (3,)
assert transfer.sum(axis=1).shape == (2,)
assert np.array_equal(transfer.sum(axis=0), [35, 70, 105])
assert np.array_equal(transfer.sum(axis=1), [90, 120])
assert np.allclose(transfer / 60, [[0.25, 0.5, 0.75], [1/3, 2/3, 1.0]])
print("Non-square transfer checks passed")
Exit-check guide: For shape (2,3), sum(axis=1) has shape (2,). Multiplying a list by 2 repeats its entries; multiplying a numeric array by 2 doubles each number. A view is useful when a shared edit is intentional.
Module 8
Three valid rows remain: data/30 and two python rows totaling 45. The unknown, blank-topic, negative and fractional-minute rows are rejected. No unknown value is invented.
# Reference answer. Attempt the exercise first.
from io import StringIO
import pandas as pd
def clean_session_table(csv_text):
raw = pd.read_csv(StringIO(csv_text), dtype="string", keep_default_na=False)
clean = raw.copy()
clean["topic"] = clean["topic"].str.strip().str.lower()
clean["minutes"] = pd.to_numeric(clean["minutes"], errors="coerce")
valid = (clean["topic"].ne("") & clean["minutes"].notna()
& clean["minutes"].ge(0) & clean["minutes"].mod(1).eq(0))
return clean.loc[valid].copy(), raw.loc[~valid].copy()
def topic_totals(accepted):
return accepted.groupby("topic", as_index=False)["minutes"].sum().sort_values("topic")
text = "topic,minutes\n Python ,20\ndata,30\npython,25\ncharts,unknown\n,10\ndata,-3\ncharts,1.5\n"
accepted, rejected = clean_session_table(text)
summary = topic_totals(accepted)
assert len(accepted) == 3 and len(rejected) == 4
assert summary["topic"].tolist() == ["data", "python"]
assert summary["minutes"].tolist() == [30, 45]
print(summary.to_string(index=False))
# Limitation: rejected durations prevent a complete practice-time total.
# Question: can the source owner recover missing or invalid duration values?
# The separate inspection fixture: record observations before cleaning it.
inspection_text = "topic,minutes\n arrays ,15\ntables,not yet\ntables,0\n charts ,-4\n,20\n"
inspection_raw = pd.read_csv(StringIO(inspection_text), dtype="string", keep_default_na=False)
print("Inspection shape:", inspection_raw.shape)
print(inspection_raw.head().to_string())
print("Source missing markers:", inspection_raw.isna().sum().to_dict())
# Observations: topic spacing needs normalization; 'not yet' needs review;
# one empty topic is not a category, and -4 violates the duration rule.
inspection_accepted, inspection_rejected = clean_session_table(inspection_text)
assert inspection_accepted.index.tolist() == [0, 2]
assert inspection_accepted["minutes"].tolist() == [15, 0]
print("Inspection transfer checks passed")
Exit-check guide: Parsing creates structured fields; validation applies the task contract. Grouping changes one row per session to one row per topic. count can exclude missing values, while row count counts rows. Preserve rejects so another person can review each decision.
Module 9
The bar heights are 80, 70 and 30, with minutes on a zero-based axis. Exact labels and a synthetic-data title make the display easy to interpret. The image must still be visually inspected.
# Reference answer. Attempt the exercise first.
# Profile: Charts · Matplotlib
import matplotlib.pyplot as plt
topics = ["Python", "Data", "Charts"]
minutes = [80, 70, 30]
fig, ax = plt.subplots(figsize=(6, 3.5), layout="constrained")
ax.bar(topics, minutes, color=["#2563eb", "#0d9488", "#d97706"])
ax.set(title="Practice time by topic (synthetic sessions)",
xlabel="Topic", ylabel="Minutes", ylim=(0, 90))
ax.spines[["top", "right"]].set_visible(False)
for topic, value in zip(topics, minutes):
ax.text(topic, value + 2, str(value), ha="center")
fig.savefig("practice_minutes.png", dpi=160)
# The lab captures the chart and offers a download. Locally, open the PNG.
# For an interactive local window, optionally call plt.show() afterward.
# Text alternative: Python has 80 minutes, Data 70, Charts 30.
# These synthetic recorded durations do not measure learning gains.
Exit-check guide: A bar chart compares category totals. A truncated baseline can exaggerate their difference. Execution alone does not check clipping or readability. Practice time cannot establish learning gains or causation.
Module 10 · Capstone
The complete local reference is solutions/capstone.py. The single-file Data-profile reference is examples/10_capstone_data.py; the separate Charts-profile file is examples/10_capstone_chart.py. The key choices are to preserve raw records, remove only exact duplicate extras, reject every conflicting-ID record and keep a reason for invalid input. See the test suite for changed-input cases rather than hard-coding the supplied totals.
Sample interpretation (reference answer)
I reviewed 16 synthetic workshop-order rows under a documented small-data policy. One exact repeated row was set aside. Of the 15 remaining records, five were rejected for missing units, negative units, an invalid price, an invalid date or a missing category. Ten records were accepted. These counts reconcile to the original row count, and the rejected and duplicate exports preserve the source values for review.
Accepted records contain 46 units and a subtotal of 6,690 cents, equivalent to 66.90 fictional currency units. Notebooks contribute 3,500 cents, pens 1,840 cents and markers 1,350 cents. The bar chart shows those category subtotals with a zero baseline and labels the synthetic source. Its text alternative states the same values so the result is understandable without the image.
This is an accepted-record subtotal, not a complete sales total. Unresolved rejected records could change the result, and the dataset does not model tax, shipping, returns or a real currency. I would ask the source owner to recover the invalid fields and explain any conflicting identifiers before using the result for a decision. Rerunning the script from the unchanged CSV and repeating the regression tests reproduces this output.
Review prompts
- An empty input with correct headers should return empty tables and zero audit counts.
- A zero price is valid under the contract, but zero units are not.
- Two different records with the same normalized ID must both be rejected.
- Adding three pens at 80 cents raises the accepted subtotal by 240 cents, provided the new ID and other fields are valid.
- The raw source must remain unchanged after every run.