Intermediate Python

Reverse a sequence for display

Call the function and then print both the original list and the returned value. Has the original list changed?

i
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.

Shows: aliasing and the difference between in-place and copy operations.

To find it: print id(original_list) before the call and id(returned_list) after. If both print the same number, the function returned the same object rather than a copy, and reversing it also mutated the original.

Track items added to each shopping cart

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?

i
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.

Shows: the difference between shared mutable class attributes and per-instance attributes initialized in __init__.

To find it: create two accounts, add one transaction to the first and a different one to the second, then print account1.history and account2.history. If both lists contain both transactions, confirm the sharing with print(Account.history is account1.history) — it will print True.

Check whether a running total hits a target

Run this script and examine the two computed values. Are they exactly equal? Try printing each value with many decimal places.

i
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.

Shows: floating-point representation errors and how to use math.isclose.

To find it: print both values with 20 decimal places using f"{val:.20f}". You will see that one ends in ...000001 rather than matching the other exactly, revealing the rounding difference that makes == return False.

Load settings from a configuration file

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.

i
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.

Shows: how overly broad exception handlers swallow unrelated bugs, and how to use logging.exception to record errors instead of ignoring them.

To find it: replace pass in the except block with logging.exception("caught"). Run the script again and read the log output. You will see a ValueError from the URL parser printed for the malformed URL, proving the except block was swallowing the wrong exception type.

Read addresses from a spreadsheet export

Run the script with the provided CSV file and read the traceback. Which line in the file triggers the error? Examine that line carefully.

i
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})")
i
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.

Shows: why hand-rolled CSV parsing fails on real data and when to use the csv module.

To find it: open the CSV in a text editor and find the row that triggers the IndexError. Count the commas on that line — there are two, not one. The extra comma sits inside a quoted name field that line.split(',') does not recognize as quoted.

Rank files by version number

Run the sort function and examine the output order. Where does file10 appear relative to file2?

i
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.

Shows: 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.

To find it: run the sort and search for file10 in the output. It appears immediately after file1, before file2. That placement is the signature of alphabetical order, where "10" < "2" because "1" < "2" at the first character.

Process a pipeline of records twice

Run the script and look at both outputs. Does each one produce the values you expected?

i
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.

Shows: that generators are single-use iterators and when to use lists instead.

To find it: print type(pipeline) to confirm it is a generator. Then assign results = list(pipeline) and print results a second time — the second access returns an empty list, proving the generator was exhausted on the first pass.

Cache results of an expensive calculation

Call the cached function twice with the same positional argument but a different keyword argument each time. Do both calls return the correct result?

i
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.

Shows: how to construct correct cache keys and test with varied inputs.

To find it: call the cached function twice with the same positional argument but different keyword arguments — e.g., f(1, multiplier=2) then f(1, multiplier=3). If both return the same value, add print(key) inside the decorator to show the key is identical for both calls.

Extend a base class with new attributes

Create an instance of the subclass and try to access an attribute that is set in the parent's __init__. Does it exist?

i
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.

Shows: Python's method resolution order and how to use super() correctly.

To find it: instantiate the subclass and immediately try to access a parent-defined attribute. The AttributeError names the missing attribute. Search the subclass __init__ for a call to super().__init__() — its absence is the cause.

Write results to disk when processing fails

Run the script so that it raises an exception part-way through writing. Then open the output file. Does it contain complete data?

i
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}")
i
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.

Shows: why context managers guarantee file cleanup even when exceptions occur, and how to use with open(…) as f to prevent data loss.

To find it: run the script so that it raises an exception midway through writing, then open the output file in a text editor. Count the records. If the count is less than expected, the buffer was never flushed — close() was never called because the exception skipped it.