Basic Python

Off-by-One in Sliding Window

Run this code with a small list (for example, five elements and a window size of three) and count the windows by hand. Does the number of windows the code returns match the number you counted?

def sliding_windows(data, k):
    """Return all sliding windows of size k over data."""
    return [data[i : i + k] for i in range(len(data) - k)]


if __name__ == "__main__":
    data = [1, 2, 3, 4, 5]
    k = 3
    windows = sliding_windows(data, k)
    print(f"Windows: {windows}")
    print(f"Got {len(windows)}, expected {len(data) - k + 1}")
Show explanation

The bug is range(len(data) - k) instead of range(len(data) - k + 1), so the last window is never produced. Teaches how to identify off-by-one errors in index arithmetic and how to verify boundary conditions with small, hand-checkable examples.

String Concatenation Instead of Addition

Run this script with the provided input file and examine the total it prints. Does the value look like a reasonable sum of exam scores?

import sys


def average_scores(filename):
    """Return the average of numeric scores stored one per line."""
    total = 0
    count = 0
    with open(filename) as f:
        for line in f:
            line = line.strip()
            if line:
                total = total + line
                count += 1
    return total / count


if __name__ == "__main__":
    filename = sys.argv[1] if len(sys.argv) > 1 else "catadd.txt"
    print(f"Average: {average_scores(filename)}")
85
92
78
90
88
Show explanation

The bug is accumulating scores with total = total + line.strip() (string concatenation) instead of converting each line to a number first, so the script always reports a nonsensical total. Teaches the difference between string + and numeric +, and how to check the type of a value at runtime using type() or isinstance().

Boolean Logic in Validation

Call the validation function with several passwords, including one you expect to be accepted. Does it accept any of them?

def is_valid_password(password):
    """Return True if password is at least 8 characters and contains a digit."""
    has_length = len(password) >= 8
    has_digit = any(c.isdigit() for c in password)
    if not has_length and not has_digit:
        return False
    return True


if __name__ == "__main__":
    tests = [
        ("abc",      False),  # too short, no digit      — should be rejected
        ("abcdefgh", False),  # long enough but no digit — should be rejected
        ("abc1",     False),  # has digit but too short  — should be rejected
        ("abcdefg1", True),   # valid: long enough and has a digit
    ]
    for password, expected in tests:
        result = is_valid_password(password)
        status = "OK  " if result == expected else "FAIL"
        print(f"{status} is_valid_password({password!r}) = {result} (expected {expected})")
Show explanation

The bug is joining the two conditions with and instead of or, which requires both to fail simultaneously and almost never happens, so valid passwords are always rejected. Teaches how boolean logic errors cause silent misbehavior, and how a small truth table reveals which operator is correct.

Misremembered Conversion Formula

Call the conversion function with a coordinate you can verify by hand—for example, 1 degree, 30 minutes, 0 seconds should equal 1.5 decimal degrees. Does the function return the correct value?

def dms_to_decimal(degrees, minutes, seconds):
    """Convert degrees/minutes/seconds to decimal degrees."""
    return degrees + minutes / 60 + seconds / 60


if __name__ == "__main__":
    tests = [
        (1, 30, 0,    1.5),   # 1°30'0"  = 1.5°
        (0, 0,  3600, 1.0),   # 0°0'3600" = 1.0°
        (45, 15, 30,  45.2583333),
    ]
    for deg, mins, secs, expected in tests:
        result = dms_to_decimal(deg, mins, secs)
        print(f"{deg}°{mins}'{secs}\" = {result:.7f}  (expected {expected})")
Show explanation

The bug is dividing seconds by 60 instead of 3600 (a misremembered formula), so the function gives wrong results. Teaches how to verify formulas against known values (e.g., 1°30′0″ = 1.5°) and how to add assertion checks for values that must fall within a known range.

In-Place Sort Returns None

Run this script and examine what it prints. Is the list what you expected?

def squared_sorted(numbers):
    """Return a sorted list of squares of the input numbers."""
    squared = [x**2 for x in numbers]
    squared = squared.sort()
    return squared


if __name__ == "__main__":
    result = squared_sorted([3, 1, 4, 1, 5, 9, 2, 6])
    print(f"Result: {result}")
Show explanation

The bug is calling list.sort() (which returns None) and assigning the result, so the list is always empty. Teaches that many list methods mutate in place and return None.

Misindented Loop State Update

Run this script with the provided input file and examine the run-length counts. Then trace through the loop by hand with a short example—track what the "previous line" variable holds at each step.

import sys


def count_runs(filename):
    """Return a list of (value, count) pairs for runs of identical lines."""
    with open(filename) as f:
        lines = [line.rstrip("\n") for line in f]

    runs = []
    prev = ""
    run_count = 0
    i = 0
    while i < len(lines):
        line = lines[i]
        if line != prev:
            if run_count > 0:
                runs.append((prev, run_count))
            run_count = 0
        run_count += 1
        i += 1
    prev = line

    if run_count > 0:
        runs.append((prev, run_count))
    return runs


if __name__ == "__main__":
    filename = sys.argv[1] if len(sys.argv) > 1 else "indent.txt"
    for value, count in count_runs(filename):
        print(f"{count} x {value!r}")
apple
apple
apple
banana
banana
cherry
Show explanation

The bug is that the variable storing the previous line is updated outside (after) the while loop body due to a missing level of indentation, so every line is counted as starting a new run. Teaches how indentation governs control flow in Python and how to step through a loop mentally to find where state is updated at the wrong time.

Missing Return Statement

Call the function and print its return value. Is it what you expected?

def filter_negatives(numbers):
    """Return a new list containing only the non-negative values."""
    result = []
    for n in numbers:
        if n >= 0:
            result.append(n)


if __name__ == "__main__":
    data = [-3, 1, -1, 4, -1, 5, -9, 2, -6]
    filtered = filter_negatives(data)
    print(f"Result:   {filtered}")
    print(f"Expected: [1, 4, 5, 2]")
Show explanation

The bug is a missing return statement: the function builds the result but does not return it, so it returns None. Teaches that Python functions return None by default and how to spot missing return in control flow.

KeyError in Word Counter

Run this script with the provided input file. Read the full traceback carefully. Which line raises the error, and what does the error message tell you about what is missing?

import sys


def count_words(filename):
    """Return a dictionary mapping each word to its frequency."""
    counts = {}
    with open(filename) as f:
        for line in f:
            for word in line.split():
                counts[word] += 1
    return counts


if __name__ == "__main__":
    filename = sys.argv[1] if len(sys.argv) > 1 else "nokey.txt"
    counts = count_words(filename)
    for word, count in sorted(counts.items(), key=lambda x: -x[1])[:5]:
        print(f"{word}: {count}")
to be or not to be that is the question
whether tis nobler in the mind to suffer
the slings and arrows of outrageous fortune
or to take arms against a sea of troubles
Show explanation

The bug is incrementing counts[word] without first checking whether the key exists, so the function crashes with a KeyError on the first new word it encounters. Teaches defensive dictionary access using dict.get(key, 0) or collections.defaultdict, and how to read a KeyError traceback to identify the missing key.

JSON Integers vs. String Input

Run this script with a user ID taken directly from the JSON file. Does it grant access? Use type() to examine the types of the two values being compared.

import json
import sys


def is_allowed(user_id, allowed_file):
    """Return True if user_id appears in the allowed list in allowed_file."""
    with open(allowed_file) as f:
        data = json.load(f)
    return user_id in data["allowed_ids"]


if __name__ == "__main__":
    user_id = sys.argv[1] if len(sys.argv) > 1 else "42"
    result = is_allowed(user_id, "streq.json")
    print("Access granted." if result else "Access denied.")
{
    "allowed_ids": [1, 42, 100, 256]
}
Show explanation

The bug is that the JSON file stores IDs as integers but the login ID arrives as a string from user input, and "42" != 42 in Python, so the script always reports "access denied" even for valid users. Teaches how JSON types map to Python types and why type conversion must happen explicitly at system boundaries.

Wrong Recursive Base Case

Call the function with the argument 0. Does it return the correct result?

def factorial(n):
    """Return n! for non-negative n."""
    if n > 0:
        return n * factorial(n - 1)


if __name__ == "__main__":
    for n in [5, 3, 1, 0]:
        print(f"{n}! = {factorial(n)}")
Show explanation

The bug is a base-case condition that uses > instead of >=, so calling the function with zero triggers infinite recursion and raises a RecursionError. Teaches how to identify missing or incorrect base cases in recursion.

Mutable Default Argument

Call the function twice in a row with no arguments and compare the two return values. Are they the same?

def collect_items(new_items, result=[]):
    """Append new_items to result and return it."""
    for item in new_items:
        result.append(item)
    return result


if __name__ == "__main__":
    first = collect_items(["a", "b"])
    print(f"First call:  {first}")    # ['a', 'b']

    second = collect_items(["c"])
    print(f"Second call: {second}")

    print(f"First again: {first}")
Show explanation

The bug is a mutable default argument (def f(result=[])), so every call starts with leftover items from previous calls. Teaches Python's mutable default argument trap and why None is the correct default.

Invisible Trailing Whitespace

Run this script with the provided input file. Use repr() on a field value that fails to match its expected string. Does the repr() output reveal anything that was not visible before?

import sys


def find_value(filename, target):
    """Return the value for the matching region name in a pipe-delimited file."""
    with open(filename) as f:
        for line in f:
            parts = line.rstrip("\n").split("|")
            if len(parts) >= 2:
                region = parts[0]
                value = parts[1].strip()
                if region == target:
                    return value
    return None


if __name__ == "__main__":
    filename = sys.argv[1] if len(sys.argv) > 1 else "trailing.txt"
    for target in ["North", "South", "East", "West"]:
        result = find_value(filename, target)
        if result is None:
            print(f"{target!r}: not found")
        else:
            print(f"{target!r}: {result}")
North |142
South|98
East |201
West|77
Show explanation

The bug is that certain applications pad fields with trailing spaces, so string comparisons always fail for those rows even though the values look correct to the naked eye. Teaches that real-world data often contains invisible characters, and how .strip() and repr() help diagnose string comparison failures.

String Methods Return Copies

Call the function and print the message before and after the call. Has the message changed?

def censor(message, banned_words):
    """Replace each banned word in message with '***'."""
    for word in banned_words:
        message.replace(word, "***")
    return message


if __name__ == "__main__":
    text = "The quick brown fox jumps over the lazy dog"
    banned = ["quick", "lazy"]
    result = censor(text, banned)
    print(f"Result:   {result}")
    print(f"Expected: The *** brown fox jumps over the *** dog")
Show explanation

The bug is that str.replace returns a new string and the return value is never assigned back, so the original message is unchanged at the end. Teaches that string methods never mutate their argument, and that every string transformation must be captured in a variable.

Mutating a List During Iteration

Run this script and count how many items were removed. Is it the number you expected? Try with a list where every element should be removed.

def remove_negatives(numbers):
    """Remove all negative numbers from the list in place and return it."""
    for n in numbers:
        if n < 0:
            numbers.remove(n)
    return numbers


if __name__ == "__main__":
    data = [-1, -2, 3, -4, 5]
    result = remove_negatives(data)
    print(f"Result:   {result}")
    print(f"Expected: [3, 5]")
Show explanation

The bug is modifying a list while iterating over it, which causes the loop to skip every other matching item. Teaches why mutating a collection during iteration causes unpredictable behavior.