Intermediate Python
Aliasing and In-Place Reversal
Call the function and then print both the original list and the returned value. Has the original list changed?
def reversed_copy(data):
"""Return a reversed copy of data, leaving the original unchanged."""
data.reverse()
return data
if __name__ == "__main__":
original = [1, 2, 3, 4, 5]
result = reversed_copy(original)
print(f"Result: {result}")
print(f"Original: {original}")
Show explanation
The bug is using list.reverse() (which mutates in place) instead of reversed()
or slicing, so the original list is also reversed after the call. Teaches aliasing
and the difference between in-place and copy operations.
Shared Mutable Class Attribute
Create two account objects, add different transactions to each, and then print the transaction history of each. Does each account show only its own transactions?
class BankAccount:
history = []
def __init__(self, owner):
self.owner = owner
def deposit(self, amount):
self.history.append((self.owner, "deposit", amount))
def withdraw(self, amount):
self.history.append((self.owner, "withdrawal", amount))
if __name__ == "__main__":
alice = BankAccount("Alice")
bob = BankAccount("Bob")
alice.deposit(100)
bob.deposit(50)
alice.withdraw(30)
print(f"Alice's history: {alice.history}")
print(f"Bob's history: {bob.history}")
Show explanation
The bug is that history = [] is defined at class level, so all instances share the
same list object instead of each having their own, and every account's transactions
appear in every other account. Teaches the difference between shared mutable class
attributes and per-instance attributes initialized in __init__.
Floating-Point Equality
Run this script and examine the two computed values. Are they exactly equal? Try printing each value with many decimal places.
def running_total(amounts):
"""Return the total by accumulating amounts one at a time."""
result = 0.0
for a in amounts:
result += a
return result
def direct_total(amounts):
"""Return the total by summing all amounts at once."""
return sum(amounts)
if __name__ == "__main__":
amounts = [0.1] * 10 # mathematically equals 1.0
t1 = running_total(amounts)
t2 = direct_total(amounts)
print(f"Running total: {t1!r}")
print(f"Direct total: {t2!r}")
if t1 == 1.0:
print("Running total is exactly 1.0")
else:
print(f"Running total is NOT exactly 1.0 (off by {abs(t1 - 1.0)!r})")
if t1 == t2:
print("Totals match.")
else:
print("Totals differ!")
Show explanation
The bug is using == on floats computed by different routes, so the comparison
returns False even when the values should be equal. Teaches floating-point
representation errors and how to use math.isclose.
Overly Broad Exception Handler
Run the scraper with the provided URL list, which includes one malformed URL. Does it process all the valid URLs? Check whether anything is silently discarded.
from urllib.parse import urlparse
URLS = [
"https://example.com/page1",
"https://example.com/page2",
"not-a-valid-url",
"https://example.com/page4",
"https://example.com/page5",
]
def fetch_title(url):
"""Return a simulated page title; raises ValueError for malformed URLs."""
parsed = urlparse(url)
if not parsed.scheme:
raise ValueError(f"Invalid URL: {url!r}")
return f"Title from {parsed.netloc}{parsed.path}"
def scrape_all(urls):
"""Fetch a title for each URL and return the list of results."""
titles = []
try:
for url in urls:
title = fetch_title(url)
titles.append(title)
except Exception:
pass
return titles
if __name__ == "__main__":
results = scrape_all(URLS)
valid = sum(1 for u in URLS if urlparse(u).scheme)
print(f"Got {len(results)} titles (expected {valid}):")
for t in results:
print(f" {t}")
Show explanation
The bug is wrapping the fetch-and-parse loop in try/except Exception: pass to
tolerate network timeouts. The ValueError raised by the URL parser is also caught
and discarded, so the scraper silently stops processing after the first malformed
URL. Teaches how overly broad exception handlers swallow unrelated bugs, and how to
use logging.exception to record errors instead of ignoring them.
Commas Inside CSV Fields
Run the script with the provided CSV file and read the traceback. Which line in the file triggers the error? Examine that line carefully.
import sys
def top_scorer(filename):
"""Return the (name, score) pair with the highest score."""
best_name = None
best_score = -1
with open(filename) as f:
next(f) # skip header
for line in f:
parts = line.strip().split(",")
name = parts[0]
score = int(parts[1])
if score > best_score:
best_score = score
best_name = name
return best_name, best_score
if __name__ == "__main__":
filename = sys.argv[1] if len(sys.argv) > 1 else "commas.csv"
name, score = top_scorer(filename)
print(f"Top scorer: {name} ({score})")
name,score
Alice Johnson,95
Bob Smith,87
Martinez, Carlos,92
Wong, Jennifer,88
Diana Prince,79
Show explanation
The bug is that names containing a comma (e.g., "Smith, John") cause
line.split(',') to produce three fields instead of two, so the index used for the
score points at the wrong element and the script crashes with an IndexError.
Teaches why hand-rolled CSV parsing fails on real data and when to use the csv
module.
Lexicographic vs. Numeric Sort
Run the sort function and examine the output order. Where does file10 appear
relative to file2?
def sorted_logs(filenames):
"""Return filenames sorted by their embedded sequence number."""
return sorted(filenames)
if __name__ == "__main__":
files = ["file10.txt", "file2.txt", "file1.txt", "file20.txt", "file3.txt"]
result = sorted_logs(files)
print("Sorted order:")
for f in result:
print(f" {f}")
print()
print("Expected numeric order: file1, file2, file3, file10, file20")
Show explanation
The bug is using the default sort(), which gives lexicographic order and places
file10 before file2. Teaches the difference between lexicographic and numeric
sort order and how to write a key function that extracts the embedded integer so
the files sort as file1, file2, file10.
Exhausted Generator
Run the script and look at both outputs. Does each one produce the values you expected?
def positive_numbers(data):
"""Yield each positive number from data."""
return (x for x in data if x > 0)
if __name__ == "__main__":
data = [-1, 2, -3, 4, -5, 6, -7, 8]
gen = positive_numbers(data)
total = sum(gen)
count = sum(1 for _ in gen)
print(f"Total: {total}")
print(f"Count: {count}")
if count > 0:
print(f"Mean: {total / count}")
else:
print("Cannot compute mean: generator was exhausted before count was taken")
Show explanation
The bug is that generators are exhausted after one pass, so the second use of the generator in the same expression produces no results. Teaches that generators are single-use iterators and when to use lists instead.
Incomplete Cache Key
Call the cached function twice with the same positional argument but a different keyword argument each time. Do both calls return the correct result?
def memoize(func):
"""Cache the results of func calls."""
cache = {}
def wrapper(*args, **kwargs):
key = args
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@memoize
def power(base, exponent=2):
print(f" (computing {base}^{exponent})")
return base ** exponent
if __name__ == "__main__":
print(f"power(3) = {power(3)}")
print(f"power(3, exponent=3)= {power(3, exponent=3)}")
print(f"power(3, 3) = {power(3, 3)}")
Show explanation
The bug is that the cache key does not include all function arguments (e.g., ignores keyword arguments), so the decorator returns the same result for different inputs. Teaches how to construct correct cache keys and test with varied inputs.
Forgetting super().__init__()
Create an instance of the subclass and try to access an attribute that is set in
the parent's __init__. Does it exist?
class Animal:
def __init__(self, name, sound):
self.name = name
self.sound = sound
def speak(self):
return f"{self.name} says {self.sound}"
class Dog(Animal):
def __init__(self, name):
self.tricks = []
def learn_trick(self, trick):
self.tricks.append(trick)
def show_tricks(self):
return f"{self.name} knows: {', '.join(self.tricks)}"
if __name__ == "__main__":
dog = Dog("Rex")
dog.learn_trick("sit")
dog.learn_trick("shake")
print(f"Tricks: {dog.tricks}")
print(dog.speak())
Show explanation
The bug is forgetting super().__init__(), so the parent's __init__ is never
called and required attributes are missing when a subclass method tries to use them.
Teaches Python's method resolution order and how to use super() correctly.
File Not Closed on Exception
Run the script so that it raises an exception part-way through writing. Then open the output file. Does it contain complete data?
import os
import sys
def summarize(input_file, output_file):
"""Write uppercased non-blank lines from input_file to output_file."""
out = open(output_file, "w")
with open(input_file) as f:
for line in f:
line = line.rstrip("\n")
if not line:
raise ValueError(f"Unexpected blank line in {input_file!r}")
out.write(line.upper() + "\n")
out.close()
if __name__ == "__main__":
input_file = sys.argv[1] if len(sys.argv) > 1 else "unclosed.txt"
output_file = "unclosed_out.txt"
try:
summarize(input_file, output_file)
except ValueError as e:
print(f"Error: {e}")
if os.path.exists(output_file):
with open(output_file) as f:
lines = f.readlines()
print(f"Output has {len(lines)} line(s) — may be incomplete (buffer not flushed)")
for line in lines:
print(f" {line!r}")
first line
second line
fourth line after blank
fifth line
Show explanation
The bug is using open() without a with statement. When an unhandled exception
occurs midway through, the output file is left partially written because the write
buffer is never flushed and close() is never called. Teaches why context managers
guarantee file cleanup even when exceptions occur, and how to use
with open(...) as f to prevent data loss.