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.