Chapter 4 Exercises: Control Flow, Functions, and Thinking Like a Programmer

How to use these exercises: Work through each part in order. Parts A-D focus on Chapter 4 concepts. Part E pushes further with multi-step problems. Part M mixes in concepts from Chapters 1-3, because real programming requires everything at once.

Difficulty key: ⭐ Foundational | ⭐⭐ Intermediate | ⭐⭐⭐ Advanced | ⭐⭐⭐⭐ Extension

Coding exercises: Type and run every piece of code. Reading code is not the same as writing code. Predict the output before you run it, then check.


Part A: Conditionals ⭐

These exercises focus on if, elif, and else statements.


Exercise 4.1Basic conditional

Write an if/else statement that checks whether a variable temperature is above 100 (degrees Fahrenheit). If it is, print "Fever detected". Otherwise, print "Normal temperature". Test with temperature = 101.3 and temperature = 98.6.

Guidance
temperature = 101.3

if temperature > 100:
    print("Fever detected")
else:
    print("Normal temperature")
For `101.3`, you should see `"Fever detected"`. For `98.6`, you should see `"Normal temperature"`. The comparison operator `>` produces a boolean that the `if` statement evaluates.

Exercise 4.2Three categories

Elena wants to classify countries by GDP per capita into three tiers: "low income" (below $1,046), "middle income" ($1,046 to $12,695), and "high income" (above $12,695). These thresholds are approximate and based on World Bank classifications. Write an if/elif/else chain for a variable gdp_per_capita and test it with values 850, 5200, and 45000.

Guidance
gdp_per_capita = 5200

if gdp_per_capita < 1046:
    income_level = "low income"
elif gdp_per_capita <= 12695:
    income_level = "middle income"
else:
    income_level = "high income"

print(f"${gdp_per_capita} → {income_level}")
Pay attention to the boundary values. Is $1,046 "low" or "middle"? Your choice of `<` vs. `<=` matters. The World Bank uses the threshold as the lower bound of the next category, so `$1,046` would be "middle income."

Exercise 4.3Nested conditionals ⭐⭐

Priya is categorizing NBA players by points per game. Players scoring 20+ points are "stars." Among stars, those averaging 30+ are "superstars." Players scoring below 20 are "role players," and those below 10 are "bench players." Write a nested conditional that assigns the correct label. Test with ppg = 32, ppg = 22, ppg = 15, and ppg = 7.

Guidance
ppg = 32

if ppg >= 20:
    if ppg >= 30:
        label = "superstar"
    else:
        label = "star"
else:
    if ppg >= 10:
        label = "role player"
    else:
        label = "bench player"

print(f"{ppg} PPG → {label}")
You could also write this with a flat `if`/`elif` chain (checking `>= 30` first, then `>= 20`, etc.). Both approaches work. The flat version is often cleaner — try rewriting it that way.

Exercise 4.4Boolean expressions

Without running the code, predict whether each expression evaluates to True or False. Then verify in Python.

a = 10
b = 20
c = 10
  1. a == c
  2. a != b
  3. a > b
  4. a >= c
  5. a < b and b < 30
  6. a > b or c == 10
Guidance 1. `True` — 10 equals 10 2. `True` — 10 is not equal to 20 3. `False` — 10 is not greater than 20 4. `True` — 10 is greater than or equal to 10 5. `True` — both sides are true (10 < 20 and 20 < 30) 6. `True` — the second part is true (c == 10), and `or` only needs one side to be true

Exercise 4.5Conditional with string data ⭐⭐

Marcus receives payment types as strings: "cash", "card", or "mobile". Write a conditional that prints the processing fee: cash has no fee, card has a 2.5% fee, and mobile has a 1.5% fee. For a sale_amount = 45.00 and payment_type = "card", print the fee amount and the net amount Marcus receives.

Guidance
sale_amount = 45.00
payment_type = "card"

if payment_type == "cash":
    fee_rate = 0
elif payment_type == "card":
    fee_rate = 0.025
elif payment_type == "mobile":
    fee_rate = 0.015
else:
    print(f"Unknown payment type: {payment_type}")
    fee_rate = 0

fee = sale_amount * fee_rate
net = sale_amount - fee
print(f"Sale: ${sale_amount:.2f}")
print(f"Fee ({payment_type}): ${fee:.2f}")
print(f"Net: ${net:.2f}")
Note the `else` clause that handles unexpected payment types. Defensive programming like this will save you in data science, where real data always contains surprises.

Part B: Loops ⭐–⭐⭐

These exercises focus on for loops, while loops, and common loop patterns.


Exercise 4.6Basic for loop

Write a for loop that prints each item in this list on its own line:

regions = ["Africa", "Americas", "Southeast Asia",
           "Europe", "Eastern Mediterranean", "Western Pacific"]
Guidance
for region in regions:
    print(region)
The loop variable `region` takes on each value in the list, one at a time. You should see all six WHO regions printed, each on its own line.

Exercise 4.7Accumulator pattern

Marcus's daily sales for a week are [520, 480, 610, 430, 390, 710, 580]. Use a for loop and an accumulator to compute the total. Then compute the average by dividing by the number of days. Print both values.

Guidance
daily_sales = [520, 480, 610, 430, 390, 710, 580]
total = 0

for sale in daily_sales:
    total = total + sale

average = total / len(daily_sales)
print(f"Total: ${total}")
print(f"Average: ${average:.2f}")
Total should be $3720, average should be $531.43. Remember: initialize `total = 0` *before* the loop.

Exercise 4.8Count with condition ⭐⭐

Using Elena's vaccination rates [42.3, 65.0, 88.1, 71.5, 93.2, 38.7, 55.0, 81.4], write a loop that counts how many rates are above 70%. Also find the highest rate. Print both results.

Guidance
rates = [42.3, 65.0, 88.1, 71.5, 93.2, 38.7, 55.0, 81.4]

above_70 = 0
highest = rates[0]

for rate in rates:
    if rate > 70:
        above_70 = above_70 + 1
    if rate > highest:
        highest = rate

print(f"Rates above 70%: {above_70}")
print(f"Highest rate: {highest}%")
You should find 4 rates above 70% and the highest is 93.2%. Note that the two `if` statements are independent — both are checked for every rate. This is different from `if`/`elif`, where only one branch executes.

Exercise 4.9Loop with string formatting ⭐⭐

Write a for loop that prints each of these numbers formatted as a percentage with one decimal place:

values = [0.423, 0.65, 0.881, 0.715, 0.932]

Expected output:

42.3%
65.0%
88.1%
71.5%
93.2%
Guidance
values = [0.423, 0.65, 0.881, 0.715, 0.932]

for val in values:
    percentage = val * 100
    print(f"{percentage:.1f}%")
Recall from Chapter 3 that `:.1f` formats a float with one decimal place. You need to multiply by 100 first because the values are stored as decimals (0.423 = 42.3%).

Exercise 4.10While loop: countdown

Write a while loop that counts down from 5 to 1, printing each number, then prints "Liftoff!" after the loop ends.

Guidance
count = 5
while count >= 1:
    print(count)
    count = count - 1
print("Liftoff!")
Make sure `count` decreases on each iteration. If you forget `count = count - 1`, you'll have an infinite loop.

Exercise 4.11While loop: find first match ⭐⭐

Write a while loop that searches through the list [12, 45, 78, 23, 91, 34, 67] and stops as soon as it finds a number greater than 80. Print which number it found and at which position (index).

Guidance
numbers = [12, 45, 78, 23, 91, 34, 67]
index = 0

while index < len(numbers):
    if numbers[index] > 80:
        print(f"Found {numbers[index]} at position {index}")
        break
    index = index + 1

if index == len(numbers):
    print("No number greater than 80 found")
The `break` statement exits the loop immediately. The number 91 at index 4 should be found. The check after the loop handles the case where no match exists. (We haven't formally taught `break` in the chapter — this is intentional. Part of learning to program is discovering new tools when you need them. Python's `break` statement exits the innermost loop immediately.)

Exercise 4.12Nested loop ⭐⭐⭐

Write nested for loops that produce this multiplication table:

1 x 1 = 1
1 x 2 = 2
1 x 3 = 3
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
3 x 1 = 3
3 x 2 = 6
3 x 3 = 9
Guidance
for i in range(1, 4):
    for j in range(1, 4):
        print(f"{i} x {j} = {i * j}")
The outer loop runs 3 times (i = 1, 2, 3). For each value of `i`, the inner loop runs 3 times (j = 1, 2, 3). Total iterations: 3 x 3 = 9 print statements.

Part C: Functions ⭐–⭐⭐⭐

These exercises focus on defining and using functions.


Exercise 4.13First function

Write a function called celsius_to_fahrenheit that takes a temperature in Celsius and returns the equivalent in Fahrenheit. The formula is: F = C * 9/5 + 32. Test it with 0, 100, and 37.

Guidance
def celsius_to_fahrenheit(celsius):
    fahrenheit = celsius * 9/5 + 32
    return fahrenheit

print(celsius_to_fahrenheit(0))    # 32.0
print(celsius_to_fahrenheit(100))  # 212.0
print(celsius_to_fahrenheit(37))   # 98.6
Notice that the function does one thing — converts a temperature. It doesn't print the result; it returns it. The caller decides what to do with the return value (print it, store it, pass it to another function, etc.).

Exercise 4.14Function with conditional ⭐⭐

Write a function called pass_or_fail that takes a grade (0-100) and returns "pass" if the grade is 60 or above, and "fail" otherwise. Test with grades 59, 60, 85, and 42.

Guidance
def pass_or_fail(grade):
    if grade >= 60:
        return "pass"
    else:
        return "fail"

for g in [59, 60, 85, 42]:
    print(f"{g} → {pass_or_fail(g)}")
Note that `return` immediately exits the function. Once `return "pass"` executes, the `else` block is never reached. Some programmers write this without the `else` entirely, since the first `return` would have already exited.

Exercise 4.15Function with a loop ⭐⭐

Write a function called count_above that takes a list of numbers and a threshold, and returns how many numbers in the list are above the threshold.

# Expected usage:
count_above([42.3, 65.0, 88.1, 71.5, 93.2], 70)  # Should return 3
count_above([42.3, 65.0, 88.1, 71.5, 93.2], 90)  # Should return 1
Guidance
def count_above(numbers, threshold):
    count = 0
    for num in numbers:
        if num > threshold:
            count = count + 1
    return count

print(count_above([42.3, 65.0, 88.1, 71.5, 93.2], 70))  # 3
print(count_above([42.3, 65.0, 88.1, 71.5, 93.2], 90))  # 1
print(count_above([42.3, 65.0, 88.1, 71.5, 93.2], 95))  # 0
This combines the accumulator pattern (count), a loop, and a conditional — all inside a function. The function returns the count, leaving it to the caller to decide what to do with it.

Exercise 4.16Multiple return values ⭐⭐

Write a function called min_and_max that takes a list of numbers and returns both the minimum and maximum values. Do NOT use Python's built-in min() and max() functions — write the logic yourself with a loop.

Guidance
def min_and_max(numbers):
    smallest = numbers[0]
    largest = numbers[0]
    for num in numbers:
        if num < smallest:
            smallest = num
        if num > largest:
            largest = num
    return smallest, largest

low, high = min_and_max([42.3, 65.0, 88.1, 71.5, 93.2])
print(f"Min: {low}, Max: {high}")  # Min: 42.3, Max: 93.2
Initialize both `smallest` and `largest` to the first element. Use separate `if` statements (not `elif`) because the same number could be both the new minimum and the new maximum if the list has only one element.

Exercise 4.17Default parameters ⭐⭐

Write a function called format_currency that takes an amount and an optional currency symbol (defaulting to "$"). It should return the amount formatted with two decimal places and the currency symbol.

# Expected:
format_currency(42.5)        # "$42.50"
format_currency(42.5, "€")   # "€42.50"
format_currency(1000, "£")   # "£1000.00"
Guidance
def format_currency(amount, symbol="$"):
    return f"{symbol}{amount:.2f}"

print(format_currency(42.5))        # $42.50
print(format_currency(42.5, "€"))   # €42.50
print(format_currency(1000, "£"))   # £1000.00
The default parameter `symbol="$"` is used when the caller doesn't provide a second argument. Default parameters must come after non-default parameters in the function definition.

Exercise 4.18Functions calling functions ⭐⭐⭐

Write three functions that work together: 1. compute_average(numbers) — returns the average of a list 2. compute_deviation(value, average) — returns how far a value is from the average 3. summarize_deviations(numbers) — calls the first two functions and prints each number, its deviation from the mean, and whether it's above or below average

Test with [85, 92, 67, 78, 95].

Guidance
def compute_average(numbers):
    total = 0
    for num in numbers:
        total = total + num
    return total / len(numbers)

def compute_deviation(value, average):
    return value - average

def summarize_deviations(numbers):
    avg = compute_average(numbers)
    print(f"Average: {avg:.1f}")
    for num in numbers:
        dev = compute_deviation(num, avg)
        direction = "above" if dev > 0 else "below"
        if dev == 0:
            direction = "at"
        print(f"  {num}: {dev:+.1f} ({direction} average)")

summarize_deviations([85, 92, 67, 78, 95])
This is decomposition in action — each function does one thing, and the orchestrating function (`summarize_deviations`) coordinates them.

Part D: Debugging and Tracing ⭐⭐–⭐⭐⭐

These exercises build your ability to read, trace, and fix code.


Exercise 4.19Trace the output ⭐⭐

Without running the code, predict the exact output. Then run it to check.

x = 10
for i in range(4):
    if x > 15:
        x = x - 5
    else:
        x = x + 3
    print(x)
Guidance Trace: - i=0: x=10, 10 > 15? No → x = 10 + 3 = 13, print 13 - i=1: x=13, 13 > 15? No → x = 13 + 3 = 16, print 16 - i=2: x=16, 16 > 15? Yes → x = 16 - 5 = 11, print 11 - i=3: x=11, 11 > 15? No → x = 11 + 3 = 14, print 14 Output:
13
16
11
14

Exercise 4.20Find the bug ⭐⭐

This function is supposed to return the sum of all positive numbers in a list, but it has a bug. Find and fix it.

def sum_positive(numbers):
    total = 0
    for num in numbers:
        if num > 0:
        total = total + num
    return total

print(sum_positive([3, -1, 4, -2, 5]))  # Should print 12
Guidance The bug is a missing indentation — `total = total + num` needs to be indented inside the `if` block:
def sum_positive(numbers):
    total = 0
    for num in numbers:
        if num > 0:
            total = total + num  # Fixed: indented inside the if block
    return total
Without the indentation, Python throws an `IndentationError` because it expects an indented block after the `if` line. This is one of the most common beginner errors.

Exercise 4.21Find the bug ⭐⭐⭐

This function is supposed to return the average of a list of numbers, but it returns the wrong answer. The code runs without errors — the bug is logical, not syntactical. Find it.

def average(numbers):
    total = 0
    count = 0
    for num in numbers:
        total = total + num
    count = count + 1
    return total / count

print(average([10, 20, 30]))  # Should print 20.0
Guidance The bug: `count = count + 1` is *outside* the loop (it's not indented inside the `for` block). It runs once after the loop, so `count` is always 1. The function returns `total / 1`, which is just the total. Fix:
def average(numbers):
    total = 0
    count = 0
    for num in numbers:
        total = total + num
        count = count + 1  # Fixed: indented inside the loop
    return total / count
Alternatively, you could use `len(numbers)` instead of a manual count: `return total / len(numbers)`. The manual-count approach is fine for learning but more error-prone.

Exercise 4.22Trace a function call ⭐⭐⭐

Trace through this code and predict the final output. Pay attention to scope.

def mystery(a, b):
    if a > b:
        return a - b
    else:
        return b - a

x = 10
y = 25
result = mystery(y, x)
print(result)
Guidance When `mystery(y, x)` is called, `a = y = 25` and `b = x = 10`. Inside the function: is `25 > 10`? Yes. Return `25 - 10 = 15`. So `result = 15`, and the output is `15`. Key insight: the argument `y` is passed to parameter `a`, and `x` to `b`. The order of arguments matters, and the parameter names inside the function are independent of the variable names outside.

Part E: Multi-Step Problems ⭐⭐⭐–⭐⭐⭐⭐

These exercises require combining multiple concepts and writing longer solutions.


Exercise 4.23Data quality report ⭐⭐⭐

Write a function called data_quality_report that takes a list of values (which might include valid numbers, negative numbers, numbers above 100, and non-numeric values like strings). The function should return the counts of: valid values (numbers between 0 and 100 inclusive), out-of-range values, and non-numeric values.

Test with: [42.3, 65.0, -5, 110, "N/A", 88.1, "missing", 71.5, 200, 0]

Guidance
def data_quality_report(values):
    valid = 0
    out_of_range = 0
    non_numeric = 0

    for val in values:
        if not isinstance(val, (int, float)):
            non_numeric = non_numeric + 1
        elif val < 0 or val > 100:
            out_of_range = out_of_range + 1
        else:
            valid = valid + 1

    print(f"Total values: {len(values)}")
    print(f"  Valid: {valid}")
    print(f"  Out of range: {out_of_range}")
    print(f"  Non-numeric: {non_numeric}")
    return valid, out_of_range, non_numeric

data_quality_report([42.3, 65.0, -5, 110, "N/A", 88.1, "missing", 71.5, 200, 0])
Expected output: Total 10, Valid 4, Out of range 3, Non-numeric 2. (Note: the value 0 is valid because it's between 0 and 100 inclusive.)

Exercise 4.24Running total with threshold alert ⭐⭐⭐

Marcus wants to track cumulative sales across days and get an alert when the weekly total exceeds $3,000. Write a function track_sales(daily_sales, threshold) that prints the running total after each day and prints "THRESHOLD REACHED on day X!" when the total first exceeds the threshold.

Test with daily_sales = [520, 480, 610, 430, 390, 710, 580] and threshold = 3000.

Guidance
def track_sales(daily_sales, threshold):
    running_total = 0
    threshold_reached = False

    for day in range(len(daily_sales)):
        running_total = running_total + daily_sales[day]
        print(f"Day {day + 1}: ${daily_sales[day]} "
              f"(total: ${running_total})")
        if running_total > threshold and not threshold_reached:
            print(f"  >>> THRESHOLD REACHED on day {day + 1}!")
            threshold_reached = True

    return running_total

track_sales([520, 480, 610, 430, 390, 710, 580], 3000)
The `threshold_reached` boolean flag ensures the alert only prints once, even though the total stays above the threshold for all subsequent days. This is a common pattern: using a flag to track whether an event has already occurred.

Exercise 4.25Letter grade converter ⭐⭐⭐

Write a complete grading program with three functions: 1. numeric_to_letter(grade) — converts a numeric grade to a letter (A: 90-100, B: 80-89, C: 70-79, D: 60-69, F: below 60) 2. letter_to_gpa(letter) — converts a letter grade to GPA points (A=4.0, B=3.0, C=2.0, D=1.0, F=0.0) 3. class_report(grades) — takes a list of numeric grades and prints each grade's letter and GPA, plus the class GPA average

Test with [85, 92, 67, 78, 95, 43, 88, 71, 56, 90].

Guidance
def numeric_to_letter(grade):
    if grade >= 90:
        return "A"
    elif grade >= 80:
        return "B"
    elif grade >= 70:
        return "C"
    elif grade >= 60:
        return "D"
    else:
        return "F"

def letter_to_gpa(letter):
    if letter == "A":
        return 4.0
    elif letter == "B":
        return 3.0
    elif letter == "C":
        return 2.0
    elif letter == "D":
        return 1.0
    else:
        return 0.0

def class_report(grades):
    total_gpa = 0
    for grade in grades:
        letter = numeric_to_letter(grade)
        gpa = letter_to_gpa(letter)
        total_gpa = total_gpa + gpa
        print(f"  {grade} → {letter} ({gpa:.1f})")
    avg_gpa = total_gpa / len(grades)
    print(f"\nClass GPA: {avg_gpa:.2f}")

class_report([85, 92, 67, 78, 95, 43, 88, 71, 56, 90])
This is a full decomposition exercise — three functions, each doing one thing, orchestrated by the third.

Exercise 4.26Pseudocode first ⭐⭐⭐

Priya wants to analyze a list of three-point shooting percentages. She needs to know: the average percentage, how many players are above average, and which player has the highest percentage. The data is:

players = ["Curry", "Thompson", "Harden", "Lillard", "Young"]
pct = [42.1, 38.5, 36.2, 39.4, 34.3]

Step 1: Write pseudocode first (in plain English). Step 2: Translate to Python. Step 3: Wrap it in a function called three_point_analysis.

Guidance Pseudocode:
COMPUTE total by summing all percentages
COMPUTE average as total / count of players
SET above_average_count to 0
SET best_player to first player
SET best_pct to first percentage

FOR each player index:
    IF percentage > average:
        INCREMENT above_average_count
    IF percentage > best_pct:
        SET best_player to current player
        SET best_pct to current percentage

PRINT average, above_average_count, best_player
Python:
def three_point_analysis(players, pct):
    total = 0
    for p in pct:
        total = total + p
    average = total / len(pct)

    above_avg = 0
    best_player = players[0]
    best_pct = pct[0]

    for i in range(len(players)):
        if pct[i] > average:
            above_avg = above_avg + 1
        if pct[i] > best_pct:
            best_pct = pct[i]
            best_player = players[i]

    print(f"Average 3PT%: {average:.1f}%")
    print(f"Players above average: {above_avg}")
    print(f"Best shooter: {best_player} ({best_pct}%)")

players = ["Curry", "Thompson", "Harden", "Lillard", "Young"]
pct = [42.1, 38.5, 36.2, 39.4, 34.3]
three_point_analysis(players, pct)

Exercise 4.27Data validation pipeline ⭐⭐⭐⭐

Write a complete data validation system for Jordan's grade data. Create four functions:

  1. is_valid_grade(value) — returns True if value is a number between 0 and 100
  2. clean_grades(raw_grades) — takes a list that might contain invalid values, returns a new list with only valid grades, and prints how many values were removed
  3. grade_statistics(grades) — returns the min, max, and average of a list of grades
  4. full_analysis(raw_grades) — orchestrates the above functions: clean, compute statistics, print a report

Test with: [85, 92, -5, 67, "absent", 78, 110, 95, None, 88]

Guidance
def is_valid_grade(value):
    if not isinstance(value, (int, float)):
        return False
    if value < 0 or value > 100:
        return False
    return True

def clean_grades(raw_grades):
    cleaned = []
    removed = 0
    for grade in raw_grades:
        if is_valid_grade(grade):
            cleaned.append(grade)
        else:
            removed = removed + 1
    print(f"Removed {removed} invalid values")
    return cleaned

def grade_statistics(grades):
    total = 0
    smallest = grades[0]
    largest = grades[0]
    for g in grades:
        total = total + g
        if g < smallest:
            smallest = g
        if g > largest:
            largest = g
    average = total / len(grades)
    return smallest, largest, average

def full_analysis(raw_grades):
    print(f"Raw data: {len(raw_grades)} values")
    cleaned = clean_grades(raw_grades)
    print(f"Clean data: {len(cleaned)} values")
    if len(cleaned) > 0:
        low, high, avg = grade_statistics(cleaned)
        print(f"Min: {low}, Max: {high}, Average: {avg:.1f}")
    else:
        print("No valid grades to analyze!")

full_analysis([85, 92, -5, 67, "absent", 78, 110, 95, None, 88])
Note the use of `list.append()` — we haven't formally covered this yet (that's Chapter 5), but it adds an item to the end of a list. It's used here because the problem naturally requires building a new list.

Part M: Mixed Review (Chapters 1-3) ⭐⭐–⭐⭐⭐

These exercises deliberately combine Chapter 4 concepts with material from earlier chapters.


Exercise 4.28Types and conditionals ⭐⭐

(Mixes Chapter 3 data types with Chapter 4 conditionals)

Write a function called describe_type that takes any value and returns a string describing it. If it's an int, return "integer". If it's a float, return "decimal number". If it's a str, return "text". If it's a bool, return "true/false value". For anything else, return "unknown type".

Important: Test with True before testing with 1. What happens, and why? (Hint: recall from Chapter 3 that bool is a subtype of int in Python.)

Guidance
def describe_type(value):
    if isinstance(value, bool):
        return "true/false value"
    elif isinstance(value, int):
        return "integer"
    elif isinstance(value, float):
        return "decimal number"
    elif isinstance(value, str):
        return "text"
    else:
        return "unknown type"

print(describe_type(42))       # integer
print(describe_type(3.14))     # decimal number
print(describe_type("hello"))  # text
print(describe_type(True))     # true/false value
print(describe_type(1))        # integer
The `bool` check MUST come before the `int` check because `True` and `False` are technically integers in Python (`True == 1` and `False == 0`). If you check `int` first, booleans will be classified as integers. Order matters in `if`/`elif` chains!

Exercise 4.29F-strings in loops ⭐⭐

(Mixes Chapter 3 f-strings with Chapter 4 loops)

Create a function called multiplication_table that takes a number and prints its multiplication table from 1 to 10, using f-strings for formatting. Each line should be right-aligned so the numbers line up.

# Expected output for multiplication_table(7):
#  7 x  1 =   7
#  7 x  2 =  14
#  7 x  3 =  21
# ...
#  7 x 10 =  70
Guidance
def multiplication_table(n):
    for i in range(1, 11):
        product = n * i
        print(f"{n:2d} x {i:2d} = {product:3d}")

multiplication_table(7)
The format specifiers `:2d` and `:3d` right-align the integers in fields of width 2 and 3, respectively. This is a Chapter 3 concept (f-string formatting) applied inside a Chapter 4 structure (for loop inside a function).

Exercise 4.30The data science lifecycle in code ⭐⭐⭐

(Mixes Chapter 1 concepts with Chapter 4 programming)

Recall from Chapter 1 that the data science lifecycle has six stages: Ask, Acquire, Clean, Explore, Model, Communicate. Write a program that simulates a mini-lifecycle:

  1. Ask: Print the question: "Which days have above-average sales?"
  2. Acquire: Start with this data: sales = [420, 380, 510, 475, 390, 620, 550]
  3. Clean: Write a function that validates each value is a positive number
  4. Explore: Write a function that computes the average and counts days above/below average
  5. Communicate: Write a function that prints a formatted report

Wrap the whole thing in a run_analysis(data) function.

Guidance
def validate_sales(sales):
    """Clean: Remove invalid values."""
    valid = []
    for s in sales:
        if isinstance(s, (int, float)) and s > 0:
            valid.append(s)
    return valid

def explore_sales(sales):
    """Explore: Compute statistics."""
    total = 0
    for s in sales:
        total = total + s
    average = total / len(sales)
    above = 0
    below = 0
    for s in sales:
        if s > average:
            above = above + 1
        else:
            below = below + 1
    return average, above, below

def report_sales(average, above, below, total_days):
    """Communicate: Print formatted report."""
    print("=" * 35)
    print("SALES ANALYSIS REPORT")
    print("=" * 35)
    print(f"Question: Which days have above-")
    print(f"          average sales?")
    print(f"Days analyzed: {total_days}")
    print(f"Average daily sales: ${average:.2f}")
    print(f"Days above average: {above}")
    print(f"Days below average: {below}")
    print("=" * 35)

def run_analysis(data):
    """Orchestrate the full lifecycle."""
    clean_data = validate_sales(data)
    avg, above, below = explore_sales(clean_data)
    report_sales(avg, above, below, len(clean_data))

sales = [420, 380, 510, 475, 390, 620, 550]
run_analysis(sales)
This exercise connects the abstract lifecycle from Chapter 1 to concrete code from Chapter 4. Each function maps to a lifecycle stage.