Appendix G: Answers to Selected Exercises

This appendix provides worked solutions for selected exercises from Chapters 1 through 10. For Parts A and B, odd-numbered exercises are solved. For Parts C and D, selected exercises are included where the solution demonstrates an important pattern or technique. Solutions include explanations of reasoning, not just final answers.


Chapter 1: Welcome to Computer Science

A.1

Computer science is to programming as music theory is to playing an instrument. A musician can play notes without understanding harmony, chord progressions, or compositional structure — but understanding theory makes them a fundamentally better musician. It lets them improvise, compose, and understand why certain combinations sound good. Similarly, programming is the instrument, but computer science is the theory — it teaches you how to think about problems, evaluate solutions, and understand why certain approaches work better than others. You can write code without CS, but you won't know why your code is slow, how to design systems that scale, or how to solve problems you've never seen before.

A.3

"I understand the appeal of that idea, but consider this: AI tools generate code based on patterns in existing code — they don't understand the problem you're solving. Someone still needs to decompose the problem, evaluate whether the AI's output is correct, and design the overall system architecture. That 'someone' needs to understand CS fundamentals. It's like saying 'I don't need to understand math because I have a calculator.' The calculator helps, but you still need to know what to calculate and whether the answer makes sense."

A.5

  • a) Chrome web browser — software (a program you install and run)
  • b) Laptop's touchscreen — hardware (physical component)
  • c) Wi-Fi adapter — hardware (physical component, even if it has firmware)
  • d) Microsoft Word — software (application program)
  • e) Computer's RAM — hardware (physical memory chip)
  • f) An app on your phone — software (program running on the device)

B.1 (Example: Planning a road trip across three states)

Decomposition into sub-problems:

  1. Choose the route — which states, which highways, what order
  2. Estimate travel time — total driving hours, how to split across days
  3. Plan overnight stops — find hotels or campgrounds, make reservations
  4. Budget the trip — gas costs, food, lodging, activities
  5. Prepare the vehicle — oil change, tire pressure, emergency kit
  6. Pack for the trip — clothes, toiletries, entertainment, snacks
  7. Plan activities at each stop — what to see, restaurant reservations

Sub-problem 4 (Budget) can be further decomposed: - Estimate gas costs (distance / mpg * price per gallon) - Estimate food costs (meals per day * cost per meal * number of days) - Estimate lodging costs (cost per night * number of nights) - Add contingency buffer (10-20% of total)

B.5

Algorithm A (linear search): In the worst case, you'd check all 1,000 pages.

Algorithm B (binary search / "divide in half"): Each check looks at the middle page and rules out that page plus the half the word can't be in. In the worst case: - Check 1: page 500 of 1,000. At most 500 pages are left. - Check 2: the middle of what's left (page 250 or 750). At most 250 left. - Checks 3-9: 125 → 62 → 31 → 15 → 7 → 3 → 1 page left. - Check 10: that last page.

That's 10 checks. A quick way to estimate it: count how many times you can halve 1,000 before you reach 1. Since 2^10 = 1,024, the answer is about 10.

Algorithm B is dramatically faster: ~10 checks vs. ~1,000 checks. This is the difference between O(n) and O(log n), which you'll formalize in Chapter 17 and implement in Chapter 19.


Chapter 2: Getting Started

A1

REPL stands for Read-Eval-Print Loop: - Read: Python reads the expression you type - Eval: Python evaluates (executes) the expression - Print: Python prints the result to the screen - Loop: Python goes back and waits for your next input

It's an interactive cycle: type something, see the result immediately, type something else.

A3

The terminal is the window/application that provides a text-based interface — it's the container. The shell is the program running inside the terminal that interprets your commands. The terminal is like a TV screen; the shell is like the channel you're watching. On macOS, Terminal.app is the terminal, and zsh is the default shell. On Windows, Windows Terminal (or VS Code's built-in terminal panel) is the terminal, and PowerShell or cmd.exe is the shell. You can run different shells in the same terminal.

A5

Every Python error message gives you three pieces of information: 1. The file name and line number where the error occurred 2. The type of error (e.g., SyntaxError, TypeError, NameError) 3. A description of what went wrong (e.g., "unexpected indent", "name 'pritn' is not defined")

B1

print("CS", "is", "fun")

Output: CS is fun

By default, print() separates arguments with a single space.

B3

print("one", end=" ")
print("two", end=" ")
print("three")

Output: one two three

The end=" " parameter replaces the default newline with a space, so all three prints appear on the same line. The third print uses the default end="\n", so the cursor moves to the next line after "three".

B5

Assuming the user types Ada:

Hello Ada
Hello Ada

Both lines print Hello Ada, but the first uses print() with two separate arguments (separated by a space by default), while the second uses an f-string to embed name directly.


Chapter 3: Variables, Types, and Expressions

A.1

The box model says a variable is a container that holds a value. When you write x = 5, you're putting the value 5 into a box labeled x. The name tag model says a variable is a label (name tag) that points to an object in memory. When you write x = 5, you're attaching the label x to the integer object 5.

The models predict different behavior when you write:

a = [1, 2, 3]
b = a
b.append(4)
print(a)    # [1, 2, 3, 4] — a changed!

Under the box model, b = a would copy the list into a new box. Changing b wouldn't affect a. But Python uses the name tag model: b = a makes both names point to the same list object. That's why changing through b also changes what a sees.

A.3

= is the assignment operator. It binds a name to a value:

score = 95     # "score" now refers to the integer 95

== is the equality comparison operator. It tests whether two values are equal and returns True or False:

score == 95    # True — "is score equal to 95?"
score == 100   # False — "is score equal to 100?"

A.5

  • a) my_variable — legal. Uses lowercase and underscores (good Python style).
  • b) 2nd_place — illegal. Variable names cannot start with a digit.
  • c) _private — legal. Leading underscore is valid (conventionally means "private").
  • d) for — illegal. for is a reserved keyword in Python.
  • e) student-name — illegal. Hyphens are not allowed (Python interprets it as subtraction: student - name).
  • f) MAX_SIZE — legal. All caps with underscores is conventionally used for constants.
  • g) class — illegal. class is a reserved keyword.
  • h) camelCase — legal. Valid syntax, though Python convention prefers snake_case.

B.1

x = 10
y = x       # y gets the value 10 (both point to the int 10)
x = 20      # x now points to 20; y still points to 10
z = x + y   # z = 20 + 10 = 30
print(f"x = {x}, y = {y}, z = {z}")

Output: x = 20, y = 10, z = 30

Key insight: y = x attaches the name y to whatever object x refers to at that moment (the integer 10). The later line x = 20 doesn't change that object; it moves the x name tag to a different object, 20. Nothing ever touched y, so it still points to 10. (Rebinding one name never affects another name. Compare A.1 above, where b.append(4) changed the shared list itself instead of moving a name tag.)

B.3

The bug is a TypeError. The input() function always returns a string, so temperature is the string "98.6", not the number 98.6. You can't subtract 32 from a string, and Python says so: TypeError: unsupported operand type(s) for -: 'str' and 'int'.

Fix:

temperature = float(input("Enter temperature in Fahrenheit: "))
celsius = (temperature - 32) * 5 / 9
print(f"{temperature}°F = {celsius:.1f}°C")

C.1

current_year = 2026   # update this one line next year

name = input("Enter your name: ")
birth_year = int(input("Enter your birth year: "))
age = current_year - birth_year
print(f"{name}, you are approximately {age} years old.")

Naming the year means the formula reads like the idea behind it, and next year you change one line instead of hunting for a bare 2026 buried in the math. "Approximately" is honest: if the user's birthday hasn't happened yet this year, they're one year younger than this says.

C.3

total_seconds = int(input("Enter total seconds: "))
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
print(f"{total_seconds} seconds = {hours} hour(s), {minutes} minute(s), {seconds} second(s)")

The key operations: - // 3600 gives full hours (integer division) - % 3600 gives the leftover seconds after removing full hours - // 60 on that remainder gives full minutes - % 60 gives the final remaining seconds

C.5

bill = float(input("Enter the bill amount: "))
tip_percent = float(input("Enter tip percentage: "))
tip_amount = bill * tip_percent / 100
total = bill + tip_amount
print(f"Tip amount: ${tip_amount:.2f}")
print(f"Total: ${total:.2f}")

Chapter 4: Conditionals

A.1

x = 7
if x > 5:       # True — 7 > 5
    print("A")   # prints "A"
if x > 10:      # False — 7 is not > 10
    print("B")   # skipped
print("C")       # always prints — not inside any if block

Output:

A
C

Note that these are two separate if statements, not an if-elif chain. Both are evaluated independently. "C" prints regardless because it's not indented under either if.

A.3

  • a) True and False → False (both must be true for and)
  • b) True or False → True (at least one must be true for or)
  • c) not True → False
  • d) True and True and False → False (the last False makes the whole thing false)
  • e) False or False or True → True (the last True makes it true)
  • f) not (True and False) → not False → True
  • g) True or False and False → True or (False and False) → True or False → True (Note: and has higher precedence than or, so False and False is evaluated first)
  • h) (True or False) and False → True and False → False (parentheses change the evaluation order)

A.5

The bug is using = (assignment) instead of == (comparison):

grade = input("Enter your grade (A/B/C/D/F): ")
if grade = "A":      # SyntaxError! = is assignment, not comparison
    print("Excellent!")

Fix: if grade == "A":

Recent versions of Python even point at the fix: the error message ends with Maybe you meant '==' or ':=' instead of '='? (Ignore := for now; == is the one you want.)

B.1

number = float(input("Enter a number: "))

if number > 0:
    print(f"{number} is positive.")
elif number < 0:
    print(f"{number} is negative.")
else:
    print(f"{number} is zero.")

B.3

temp = float(input("Enter temperature in Fahrenheit: "))

if temp >= 90:
    print("It's hot! Wear light clothing and stay hydrated.")
elif temp >= 70:
    print("Nice weather! A t-shirt should be fine.")
elif temp >= 50:
    print("It's cool. Grab a jacket.")
elif temp >= 32:
    print("It's cold! Wear a warm coat.")
else:
    print("Freezing! Bundle up — hat, gloves, heavy coat.")

Note the elif chain tests from highest to lowest. The order matters — since we check >= 90 first, by the time we reach >= 70, we already know the temperature is below 90, so that branch covers 70 up to (but not including) 90, decimals like 89.5 included. That's why each elif needs only a lower bound.

B.5

score = float(input("Enter your score (0-100): "))

if score < 0 or score > 100:
    print("Error: score must be between 0 and 100.")
elif score >= 90:
    print("A")
elif score >= 80:
    print("B")
elif score >= 70:
    print("C")
elif score >= 60:
    print("D")
else:
    print("F")

Chapter 5: Loops

A.1

for i in range(1, 21):
    print(i)

A.3

for i in range(10, 0, -1):
    print(i)
print("Blastoff!")

range(10, 0, -1) produces 10, 9, 8, ..., 1 (starts at 10, goes down to but not including 0, stepping by -1).

A.5

n = int(input("Enter a positive integer: "))
for i in range(1, n + 1):
    print(i * 5, end=" ")
print()

For n = 4, this prints: 5 10 15 20

B.1

count = 1
while count <= 10:
    print(count)
    count += 1

B.3

word_count = 0
while True:
    word = input("Enter a word (or 'quit' to stop): ")
    if word == "quit":
        break
    word_count += 1
print(f"You entered {word_count} words.")

This uses the sentinel loop pattern: loop forever, check for the exit condition inside the loop, and break when found.

B.5

number = int(input("Enter a positive integer: "))
digit_sum = 0

while number > 0:
    digit = number % 10       # extract last digit
    digit_sum += digit        # add to sum
    number = number // 10     # remove last digit

print(f"Sum of digits: {digit_sum}")

For input 12345: - Iteration 1: digit = 5, sum = 5, number = 1234 - Iteration 2: digit = 4, sum = 9, number = 123 - Iteration 3: digit = 3, sum = 12, number = 12 - Iteration 4: digit = 2, sum = 14, number = 1 - Iteration 5: digit = 1, sum = 15, number = 0 → loop ends

C.1

The exercise asks for the accumulator pattern, so each result gets its own accumulator, set up before the loop and updated inside it:

total = 0
highest = float('-inf')   # smaller than any real score, so the first score always wins
lowest = float('inf')     # larger than any real score

for i in range(5):
    score = float(input(f"Enter test score {i + 1}: "))
    total += score
    if score > highest:
        highest = score
    if score < lowest:
        lowest = score

average = total / 5
print(f"Total: {total}")
print(f"Average: {average:.2f}")
print(f"Highest: {highest}")
print(f"Lowest: {lowest}")

For scores 88, 92, 79, 95, 84 this prints Total: 438.0, Average: 87.60, Highest: 95.0, Lowest: 79.0.

Once you're comfortable with the pattern, Python's built-ins do the same jobs in one call each. Store the scores in a list (a preview of Chapter 8) and you can write total = sum(scores), highest = max(scores), and lowest = min(scores). Under the hood, those built-ins are running the very accumulator loops you just wrote.

C.3

sentence = input("Enter a sentence: ")
spaces = 0
for char in sentence:
    if char == " ":
        spaces += 1
word_count = spaces + 1
print(f"Word count: {word_count}")

This is a simplification — it assumes single spaces between words and no leading/trailing spaces. In Chapter 7, you'll learn len(sentence.split()), which handles these edge cases correctly.

D.1 (Coding solution for 5-wide, 4-tall rectangle)

for row in range(4):
    for col in range(5):
        print("*", end="")
    print()   # newline after each row

Output:

*****
*****
*****
*****

Chapter 6: Functions

A.1

def say_hello():
    print("Hello, World!")

say_hello()
say_hello()
say_hello()

A.3

def square(n):
    return n ** 2

# Store result in a variable
result = square(7)
print(result)           # 49

# Use directly in a print statement
print(square(12))       # 144

A.5

def print_separator(char, length):
    print(char * length)

print_separator("-", 30)    # ------------------------------
print_separator("=", 20)    # ====================
print_separator("*", 10)    # **********

B.1

def create_greeting(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"

# Just a name (uses defaults)
print(create_greeting("Alice"))                         # Hello, Alice!

# Name and custom greeting
print(create_greeting("Bob", "Howdy"))                  # Howdy, Bob!

# All three arguments
print(create_greeting("Carol", "Hey", "..."))           # Hey, Carol...

# Keyword arguments in different order
print(create_greeting(punctuation="?!", name="Dave", greeting="Yo"))  # Yo, Dave?!

B.3

def find_min_max(numbers):
    if not numbers:
        return None, None

    current_min = numbers[0]
    current_max = numbers[0]

    for num in numbers[1:]:
        if num < current_min:
            current_min = num
        if num > current_max:
            current_max = num

    return current_min, current_max

lo, hi = find_min_max([34, 12, 89, 5, 67, 23])
print(f"Min: {lo}, Max: {hi}")    # Min: 5, Max: 89

B.5

def repeat_string(text, n=2):
    return " ".join([text] * n)

print(repeat_string("ha", 3))      # ha ha ha
print(repeat_string("hey"))         # hey hey
print(repeat_string("yo", 5))      # yo yo yo yo yo

C.1

x = 10

def change_x():
    x = 20                              # creates a LOCAL variable x
    print(f"Inside function: x = {x}")  # prints 20

change_x()                              # Inside function: x = 20
print(f"Outside function: x = {x}")    # Outside function: x = 10

The function creates a local variable x that shadows the global x. The global x is never modified. When the function ends, the local x is destroyed.

C.3

Rewritten without global:

def increment(count):
    return count + 1

count = 0
count = increment(count)    # pass current value, get back new value
count = increment(count)
print(count)                 # 2

This is the preferred approach: pass data in as parameters, get results back via return. Avoid global — it makes code harder to understand and debug.


Chapter 7: Strings

A.1

message = "Computational Thinking"
  • a) message[0] → 'C' (first character)
  • b) message[13] → ' ' (the space). "Computational" has 13 letters, at indices 0-12, so index 13 is the space between the words. The 'T' is at index 14. This one catches almost everybody: counting from 0 means the 14th character lives at index 13.
  • c) message[-1] → 'g' (last character)
  • d) message[-8] → 'T' ("Thinking" is the last 8 characters, so 8th from the end is its first letter. Check: -8 is the same position as 22 - 8 = 14.)
  • e) len(message) → 22 (13 + 1 space + 8; spaces count as characters)

A.3

The code produces a TypeError: 'str' object does not support item assignment on the very first assignment, language[0] = "P". Strings are immutable: once created, their characters can't be changed in place.

Fix 1: Build a new string from slices:

language = "Jathon"
language = "Py" + language[2:]    # "Py" + "thon"
print(language)                    # Python

language[2:] is everything from index 2 onward ("thon"), so gluing "Py" on the front builds the word we want.

Fix 2: Let a string method build it for you:

language = "Jathon"
language = language.replace("Ja", "Py")
print(language)                    # Python

Notice that both fixes end with language = .... Neither one changes the original string. Each builds a new string and moves the language name tag to it. That's the real lesson: with immutable strings, you don't modify, you create and reassign.

B.1

def normalize_name(name):
    parts = name.strip().split()
    if len(parts) == 1:
        return parts[0].capitalize()
    first = parts[0].capitalize()
    last = parts[-1].capitalize()
    return f"{last}, {first}"

print(normalize_name("  alice   CHEN  "))     # Chen, Alice
print(normalize_name("BOB martinez"))          # Martinez, Bob

B.3

def remove_punctuation(text):
    punctuation = ".,!?;:'\"()-"
    result = ""
    for char in text:
        if char not in punctuation:
            result += char
    return result

print(remove_punctuation("Hello, World!"))           # "Hello World"
print(remove_punctuation("It's a test (really)."))   # "Its a test really"

B.5

def initials(full_name):
    parts = full_name.strip().split()
    result = ""
    for part in parts:
        result += part[0].upper() + "."
    return result

print(initials("Anika Patel"))            # A.P.
print(initials("mary jane watson"))        # M.J.W.
print(initials("  cher  "))               # C.

C.1

def pig_latin(word):
    vowels = "aeiouAEIOU"
    if word[0] in vowels:
        return word + "yay"
    else:
        return word[1:] + word[0] + "ay"

print(pig_latin("apple"))     # appleyay
print(pig_latin("hello"))     # ellohay
print(pig_latin("python"))    # ythonpay
print(pig_latin("is"))        # isyay

Chapter 8: Lists and Tuples

A.1

foods = ["pizza", "sushi", "tacos", "ramen", "curry"]
print(f"First: {foods[0]}")        # pizza
print(f"Last: {foods[-1]}")        # curry
print(f"Middle: {foods[2]}")       # tacos
print(f"Reversed: {foods[::-1]}")  # ['curry', 'ramen', 'tacos', 'sushi', 'pizza']

A.3

fruits = ["apple", "banana", "cherry", "date"]
print(len(fruits))            # 4
print(fruits[-2])             # "cherry"
print(fruits[1:3])            # ["banana", "cherry"]
print("banana" in fruits)     # True
print("grape" not in fruits)  # True

A.5

append() adds a single item to the end of the list. extend() adds all items from an iterable individually:

a = [1, 2, 3]
a.append(4)         # a is now [1, 2, 3, 4]
a.append([5, 6])    # a is now [1, 2, 3, 4, [5, 6]] — nested list!

b = [1, 2, 3]
b.extend([4, 5, 6]) # b is now [1, 2, 3, 4, 5, 6] — flat list

If you append a list, it becomes a single element (a nested list). If you extend with a list, each element is added individually.

B.1

def stats(numbers):
    minimum = numbers[0]
    maximum = numbers[0]
    total = 0
    for num in numbers:
        if num < minimum:
            minimum = num
        if num > maximum:
            maximum = num
        total += num
    average = total / len(numbers)
    return (minimum, maximum, average, len(numbers))

result = stats([23, 45, 12, 67, 34, 89, 56])
print(f"Min: {result[0]}, Max: {result[1]}, Average: {result[2]:.2f}, Count: {result[3]}")
# Min: 12, Max: 89, Average: 46.57, Count: 7

B.3

def find_all_indices(lst, target):
    indices = []
    for i, item in enumerate(lst):
        if item == target:
            indices.append(i)
    return indices

print(find_all_indices([3, 1, 4, 1, 5, 1], 1))   # [1, 3, 5]
print(find_all_indices([3, 1, 4, 1, 5, 1], 9))    # []

B.5

def rotate_left(lst, n):
    n = n % len(lst)        # handle n >= len(lst)
    return lst[n:] + lst[:n]

print(rotate_left([1, 2, 3, 4, 5], 2))   # [3, 4, 5, 1, 2]
print(rotate_left([1, 2, 3, 4, 5], 0))   # [1, 2, 3, 4, 5]
print(rotate_left([1, 2, 3, 4, 5], 5))   # [1, 2, 3, 4, 5]

The n % len(lst) handles edge cases where n equals or exceeds the list length.

C.1

# a) Cubes of 1 through 10
result_a = [x ** 3 for x in range(1, 11)]

# b) Lengths of words
result_b = [len(word) for word in ["hello", "world", "python"]]

# c) Multiples of 3 from 1 to 20
result_c = [n for n in range(1, 21) if n % 3 == 0]

D.1 (Selected — demonstrating tuple unpacking with books)

books = [
    ("Project Hail Mary", "Andy Weir", 2021, 476),
    ("Dune", "Frank Herbert", 1965, 412),
    ("The Hunger Games", "Suzanne Collins", 2008, 374),
    ("1984", "George Orwell", 1949, 328),
    ("Klara and the Sun", "Kazuo Ishiguro", 2021, 307),
]

# a) Books published after 2000
print("Published after 2000:")
for title, author, year, pages in books:
    if year > 2000:
        print(f"  {title} ({year})")

# b) Book with most pages
longest = books[0]
for book in books[1:]:
    if book[3] > longest[3]:
        longest = book
print(f"Longest: {longest[0]} ({longest[3]} pages)")

# c) Average page count
total_pages = sum(pages for _, _, _, pages in books)
average_pages = total_pages / len(books)
print(f"Average pages: {average_pages:.0f}")

Output:

Published after 2000:
  Project Hail Mary (2021)
  The Hunger Games (2008)
  Klara and the Sun (2021)
Longest: Project Hail Mary (476 pages)
Average pages: 379

The for title, author, year, pages in books: line is tuple unpacking at work: each 4-item tuple is split into four named variables, so the condition reads year > 2000 instead of the cryptic book[2] > 2000. In part (c), _ is the conventional name for "a value I have to unpack but don't care about."


Chapter 9: Dictionaries and Sets

A.1

capitals = {
    "France": "Paris",
    "Japan": "Tokyo",
    "Brazil": "Brasilia",
    "Canada": "Ottawa",
    "Egypt": "Cairo",
}

print(capitals["France"])                          # Paris
print(capitals.get("Germany", "Unknown"))          # Unknown
capitals["Australia"] = "Canberra"                 # add new entry
print(f"Total countries: {len(capitals)}")         # 6

A.3

info = {"name": "Alice", "age": 30, "city": "Portland"}
a = "name" in info        # True  — checks keys, and "name" is a key
b = "Alice" in info       # False — "Alice" is a value, not a key
c = 30 in info            # False — 30 is a value, not a key
d = info.get("phone", "N/A")  # "N/A" — key doesn't exist, returns default
e = len(info)             # 3

Key insight: the in operator checks keys, not values. Checking keys is fast, O(1) on average, because dictionaries use hashing. To ask whether a value is present, you'd write "Alice" in info.values(), which has to scan the values one by one.

B.1

scores = {
    "Alice": [88, 92, 85, 91],
    "Bob": [75, 80, 72, 68],
    "Carol": [95, 98, 92, 97],
    "Dave": [60, 55, 70, 65],
}

best_student = None
best_average = 0

for name, student_scores in scores.items():
    avg = sum(student_scores) / len(student_scores)
    print(f"{name}: {avg:.1f}")
    if avg > best_average:
        best_average = avg
        best_student = name
    if avg < 70:
        print(f"  Warning: {name} is below 70")

print(f"\nHighest average: {best_student} ({best_average:.1f})")

Output:

Alice: 89.0
Bob: 73.8
Carol: 95.5
Dave: 62.5
  Warning: Dave is below 70

Highest average: Carol (95.5)

Starting best_average at 0 works here because every average is positive. (Starting it at float('-inf'), as in Chapter 5's C.1, would be safe for any data.)

B.3

def merge_inventories(inv1, inv2):
    merged = dict(inv1)           # start with a copy of inv1
    for item, quantity in inv2.items():
        merged[item] = merged.get(item, 0) + quantity
    return merged

shop_a = {"sword": 3, "shield": 2, "potion": 10}
shop_b = {"shield": 1, "potion": 5, "bow": 4}
print(merge_inventories(shop_a, shop_b))
# {'sword': 3, 'shield': 3, 'potion': 15, 'bow': 4}

C.1

cubes = {n: n ** 3 for n in range(1, 11)}
print(cubes)
# {1: 1, 2: 8, 3: 27, 4: 64, 5: 125, 6: 216, 7: 343, 8: 512, 9: 729, 10: 1000}

D.1

The exercise gives you lists, so the first step is converting each one with set():

cs_students = set(["Alice", "Bob", "Carol", "Dave", "Eve"])
math_students = set(["Bob", "Dave", "Frank", "Grace"])
physics_students = set(["Carol", "Eve", "Frank", "Hank"])

# Who is in both CS and Math?
print(cs_students & math_students)               # {'Bob', 'Dave'}

# Who is in CS but not Physics?
print(cs_students - physics_students)             # {'Alice', 'Bob', 'Dave'}

# Who is in all three?
print(cs_students & math_students & physics_students)  # set() — empty! No one is in all three.

# How many unique students total?
all_students = cs_students | math_students | physics_students
print(f"Unique students: {len(all_students)}")    # 8

Sets are unordered, so your output may list the names in a different order ({'Dave', 'Bob'} is the same set). If you want a predictable order, print sorted(cs_students & math_students) instead, which gives the list ['Bob', 'Dave'].


Chapter 10: File I/O

A.1

# Write the file
with open("hello.txt", "w") as f:
    f.write("Hello, file system!\n")
    f.write("This is my first file.\n")

# Read it back
with open("hello.txt", "r") as f:
    content = f.read()
    print(content)

Each write() call writes exactly what you give it, so the \n at the end of each string is what puts the two sentences on separate lines. You'll also see a blank line after the output: the file ends with \n, and print() adds its own newline on top. print(content, end="") would avoid it.

A.3

First, create the test file:

with open("numbers.txt", "w") as f:
    for num in [10, 20, 30, 40, 50]:
        f.write(f"{num}\n")

Then read and sum:

total = 0
with open("numbers.txt", "r") as f:
    for line in f:
        total += int(line.strip())
print(f"Sum: {total}")    # Sum: 150

A.5

def append_line(filename, line):
    with open(filename, "a") as f:
        f.write(line + "\n")

# Test: three calls, three lines
append_line("log.txt", "First entry")
append_line("log.txt", "Second entry")
append_line("log.txt", "Third entry")

# Verify
with open("log.txt", "r") as f:
    print(f.read())

The key is using "a" (append mode) instead of "w" (write mode). Append mode adds to the end of the file; write mode would erase everything and start fresh.

B.1

def grep(filename, keyword):
    matches = []
    with open(filename, "r") as f:
        for line in f:
            if keyword.lower() in line.lower():
                matches.append(line.strip())
    return matches

# Test (using hello.txt from A.1):
print(grep("hello.txt", "FILE"))
# ['Hello, file system!', 'This is my first file.']

Lowercasing both the keyword and the line is what makes the search case-insensitive: "FILE" matches "file" because both become "file" before the comparison. The strip() removes each line's trailing \n so the returned strings are clean.

D.1 (Selected — CSV reading and filtering)

First, create the test file:

import csv

students = [
    {"name": "Alice", "major": "CS", "gpa": "3.9"},
    {"name": "Bob", "major": "Math", "gpa": "3.2"},
    {"name": "Carol", "major": "CS", "gpa": "3.7"},
    {"name": "Dave", "major": "Biology", "gpa": "2.8"},
    {"name": "Eve", "major": "Physics", "gpa": "3.6"},
    {"name": "Frank", "major": "CS", "gpa": "3.1"},
]

with open("students.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "major", "gpa"])
    writer.writeheader()
    writer.writerows(students)

Then read and filter:

import csv

print("Students with GPA above 3.5:")
with open("students.csv", "r", newline="") as f:
    reader = csv.DictReader(f)
    for row in reader:
        if float(row["gpa"]) > 3.5:
            print(f"  {row['name']} ({row['major']}): {row['gpa']}")

Output:

Students with GPA above 3.5:
  Alice (CS): 3.9
  Carol (CS): 3.7
  Eve (Physics): 3.6

Solutions for Chapters 11-27 are not included in this appendix. Consult your instructor or the online resources for additional solutions. For the progressive project (TaskFlow CLI), checkpoint code is available in the code/ directory of the chapters that have one.