1.4.12 · D4Python & Scientific Computing

Exercises — Reading documentation and debugging

2,294 words10 min readBack to topic

These graded problems build your doc-reading and debugging muscles, from spotting the right function to reasoning about how many checks a search costs. Each problem has a full solution hidden in a collapsible callout so you can test yourself first, then reveal.

This page extends the parent topic. Some problems lean on 1.4.1-Python-basics-syntax-and-variables, 1.4.5-File-IOand-CSV-handling, and 1.7.2-Data-preprocessing-and-cleaning.


Level 1 — Recognition

You are asked only to identify or read off information. No code changes yet.

Exercise 1.1 (L1)

Given this signature copied from the docs:

How many required arguments does linspace have, and how many optional ones? What is the default number of points it produces?

Recall Solution

An argument is required when it has no =default after it, and optional when a default value is written.

  • start, stop → no defaults → 2 required.
  • num=50, endpoint=True, retstep=False → all have defaults → 3 optional.
  • Default number of points = the default of num, which is 50.

So: 2 required, 3 optional, 50 points by default.

Exercise 1.2 (L1)

Read this traceback. On which line and file was the exception actually raised, and what type of exception is it?

Traceback (most recent call last):
  File "main.py", line 45, in <module>
    result = process_data(raw_data)
  File "utils.py", line 23, in normalize
    value = record['price'] * 1.1
KeyError: 'price'
Recall Solution

A traceback is read bottom-to-top. The very last line names the exception; the frame just above it points at the code that raised it.

  • Exception type: ==KeyError== — a dictionary was asked for a key it does not contain.
  • The failing key is 'price' (that is the error message).
  • Where it was raised: the lowest code frame → File "utils.py", line 23, in normalize.
  • The frame main.py line 45 only called the chain; it is not where the crash happened.

Answer: utils.py, line 23, KeyError.


Level 2 — Application

Now use the doc facts to decide or predict behaviour.

Exercise 2.1 (L2)

Using the default endpoint=True, what array does numpy.linspace(0, 10, num=5) produce? Give all five values and the spacing between consecutive points.

Recall Solution

linspace places num points evenly from start to stop, and because endpoint=True it includes stop. With num points there are num - 1 gaps, so Starting at and stepping by : Five values, last one is exactly stop = 10. Spacing .

Exercise 2.2 (L2)

Compare numpy.linspace(0, 10, num=5, endpoint=False) with the previous exercise. What is the new spacing, and why does dropping the endpoint change it?

Recall Solution

With endpoint=False, stop is excluded, so the num points fill only the half-open interval . Now the divisor is num (not num - 1), because the point at that used to "cap" the sequence is gone: Points: — note never appears. Why the change? endpoint=True uses num-1 gaps (both ends pinned); endpoint=False uses num gaps (only the left end pinned). Same count of points, different spacing.

Exercise 2.3 (L2)

You have the string "a1b2c" and want to split on digits but keep the digits. From the docs: re.split(pattern, string) normally discards the matched separators, unless the pattern has a capturing group. Predict the output of re.split(r'(\d+)', 'a1b2c').

Recall Solution

The docs say: "If capturing parentheses are used in the pattern, then the text of all groups is also returned." The (\d+) is a capturing group matching runs of digits, so the matched digits survive in the output list: Without the parentheses — re.split(r'\d+', 'a1b2c') — you would get ['a', 'b', 'c'] and lose the digits.


Level 3 — Analysis

Now you must find why something breaks.

Exercise 3.1 (L3)

This function returns 0.0 instead of the correct average. Locate the bug and explain the mechanism.

def calculate_average_score(students):
    total = 0
    for student in students:
        total = 0
        total += student['score']
    average = total / len(students)
    return round(average, 2)
Recall Solution

Symptom: with students = [{'name':'A','score':80},{'name':'B','score':90}] we expect but get one student's value or a wrong number. Binary-search the loop body. Print total after the loop: it equals only the last student's score, not the sum. That tells us accumulation is broken inside the loop. Root cause: total = 0 sits inside the for loop, so every iteration wipes the running sum before adding. Only the final iteration's score survives. Trace it: iteration 1 → total=0 then +8080. Iteration 2 → total=0 (resets!) then +9090. After loop total=90, average = 90/2 = 45.0. Fix: move the reset outside the loop:

total = 0
for student in students:
    total += student['score']

Now total = 170, average = 170/2 = 85.0. Correct answer: 85.0.

Exercise 3.2 (L3)

A program of lines has exactly one faulty line. Using binary-search debugging — checking the middle each time and keeping only the half that still contains the bug — what is the worst-case number of checks to isolate the single bad line?

Recall Solution

Each check halves the suspect region. The number of halvings needed to shrink candidates down to is For : Worst case: 7 checks. Compare to naive linear scanning, which averages checks — binary search is dramatically cheaper because the cost grows like , not . See the figure below.

Figure — Reading documentation and debugging

Level 4 — Synthesis

Combine doc-reading + debugging into a small pipeline decision.

Exercise 4.1 (L4)

You call df.groupby('city').mean() on a DataFrame where some rows have NaN in the city column. By default (dropna=True), those NaN-city rows vanish from the result. You have 12 rows: 5 in "Delhi", 4 in "Mumbai", and 3 with city = NaN. How many rows appear in the grouped result with dropna=True, and how many with dropna=False?

Recall Solution

groupby creates one output row per distinct group key present in the data. The knob dropna decides whether NaN counts as its own key.

  • dropna=True (default): rows with NaN city are excluded entirely. Distinct real keys = {Delhi, Mumbai}2 rows. (The 3 NaN rows are dropped.)
  • dropna=False: NaN becomes its own group. Distinct keys = {Delhi, Mumbai, NaN}3 rows.

Notice the total input rows (12) never changes the number of groups — only the number of distinct keys does. Grouping collapses many rows into one row per key.

Exercise 4.2 (L4)

Continuing 4.1, if instead you wanted the count of rows per city (including NaN as its own group), what would the three counts be, and do they sum back to 12?

Recall Solution

Use df.groupby('city', dropna=False).size(). Each group's size is just how many input rows carried that key:

  • Delhi
  • Mumbai
  • NaN

Sum: . ✅ Because size() partitions every row into exactly one group when dropna=False, the counts must total the original row count. (With dropna=True they would sum to .)


Level 5 — Mastery

Full doc-reading + debugging + cost reasoning, end to end.

Exercise 5.1 (L5)

A logging system doubles the number of lines it must scan every day: day 0 has line, day 1 has , day 2 has , and so on, so day has lines. Using binary-search debugging (cost checks), how many checks does a bug hunt cost on day 10? Then argue in one sentence why binary search keeps this "sane" even as logs explode.

Recall Solution

On day the log has lines. The doubling of lines adds only one extra check per day, because . So while the data grows exponentially, the debugging cost grows only linearly in the day number — that is the whole point of a strategy: taming exponential growth into gentle linear effort.

Exercise 5.2 (L5)

You must isolate a bug in a 10,000-line file. (a) Give the worst-case number of binary-search checks. (b) Compare it to the average-case cost of a linear scan. (c) State the ratio, rounded to the nearest whole number, of linear-average to binary-worst.

Recall Solution

(a) Binary search worst case: (Because .) (b) Linear scan average = checks. (c) Ratio: So systematic binary-search debugging is roughly 357× cheaper on average than random linear scanning for a file this size. The figure earlier makes this gap visible: the curve barely rises while the line rockets upward.


Recall Quick self-test (reveal after trying)

linspace(0,10,num=5) spacing? ::: linspace(0,10,num=5,endpoint=False) spacing? ::: re.split(r'(\d+)','a1b2c') output? ::: ['a','1','b','2','c'] Fixed calculate_average_score on [80, 90]? ::: 85.0 Binary-search checks for ? ::: Groups from 5 Delhi + 4 Mumbai + 3 NaN, dropna=False? ::: rows summing to Binary-search checks for ? :::


Where this connects

Read docs signature

Predict behaviour

Run code

Read traceback bottom to top

Binary search the bug

Re-read docs and refine

  • Doc-reading feeds directly into 1.4.10-Introduction-to-matplotlib and 2.3.4-Hyperparameter-tuning-grid-search-random-search, where every knob you tune is a documented parameter.
  • The NaN-group trap ties into 1.7.2-Data-preprocessing-and-cleaning.
  • File-based tracebacks (like utils.py line 23) show up constantly in 1.4.5-File-IOand-CSV-handling.