Basic Python

Calculate a running average over a time series

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?

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

Shows: how to identify off-by-one errors in index arithmetic and how to verify boundary conditions with small, hand-checkable examples.

To find it: call the function with data = [1, 2, 3, 4, 5] and k = 3, then list every expected window by hand — [1,2,3], [2,3,4], [3,4,5] — giving three windows. Print len(range(len(data) - k)) to see it returns 2. The mismatch between 3 and 2 reveals the off-by-one.

Add up totals from a text file

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?

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

Shows: the difference between string + and numeric +, and how to check the type of a value at runtime using type() or isinstance().

To find it: print type(total) after the first iteration. You will see <class 'str'>, not <class 'int'>. Alternatively, print repr(total) after two iterations to see the digits concatenated as a string rather than added as numbers.

Validate a user registration form

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

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

Shows: how Boolean logic errors cause silent misbehavior, and how a small truth table reveals which operator is correct.

To find it: write a two-row truth table for a password that satisfies one condition but not the other — say, correct length but no special character. With and, both conditions must be false for the overall check to return True; valid passwords almost never satisfy that, so the gate always rejects.

Convert temperatures between units

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?

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

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

To find it: call dms_to_decimal(0, 0, 3600), which represents exactly one degree expressed entirely in seconds. The correct result is 1.0; if the function divides seconds by 60, it returns 60.0, making the wrong constant visible without needing a reference table.

Sort a list of student scores

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

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

Shows: that many list methods mutate in place and return None.

To find it: print sorted_scores immediately after the assignment. The output None shows the return value of list.sort() rather than a sorted list.

Count words across multiple files

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.

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

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

To find it: add print(f"prev={prev!r}") as the first line inside the loop body. Run with the sample file and watch the printed value — it never changes, which means the update is happening outside the loop rather than at the start of each iteration.

Check whether a number is prime

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

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

Shows: that Python functions return None by default and how to spot missing return in control flow.

To find it: print the function's return value directly — print(is_prime(7)). Seeing None identifies a missing return; follow the control flow to find the branch that computes the result without returning it.

Count word frequencies in a document

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?

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

Shows: defensive dictionary access using dict.get(key, 0) or collections.defaultdict, and how to read a KeyError traceback to identify the missing key.

To find it: run the script on the sample file and read the traceback from bottom to top. The last line shows KeyError: 'some_word', naming the exact key that was missing. The line above it in the traceback shows counts[word] += 1, which is where the crash happened — the key was used before it was created.

Match user IDs loaded from a config file

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.

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

Shows: how JSON types map to Python types and why type conversion must happen explicitly at system boundaries.

To find it: print type(user_id) and type(allowed_ids[0]) side by side. You will see <class 'str'> and <class 'int'> on consecutive lines; that mismatch explains why == always returns False.

Compute factorials recursively

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

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

Shows: how to identify missing or incorrect base cases in recursion.

To find it: call factorial(0) directly and read the RecursionError. Then read the base-case condition: if n > 0 means 0 > 0 is False, so the function recurses instead of returning 1. Replacing > with >= fixes it.

Collect results with a helper function

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

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

Shows: Python's mutable default argument trap and why None is the correct default.

To find it: call collect() twice with no arguments and print both return values on the same line — print(collect(), collect()). The second list will contain all items from the first call plus new ones, proving the two calls share the same underlying list.

Compare lines read from a file

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?

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

Shows: that real-world data often contains invisible characters, and how .strip() and repr() help diagnose string comparison failures.

To find it: print repr(row[field]) for a row that fails the comparison. You will see trailing spaces — e.g., 'Smith ' — that are invisible in a normal print but visible in the repr() output.

Clean up user-submitted text

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

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

Shows: that string methods never mutate their argument, and that every string transformation must be captured in a variable.

To find it: print message both before and after calling clean_message. If the message is unchanged, the transformation's return value was discarded. Check that the result of str.replace is assigned back to a variable.

Remove expired entries from a list

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.

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

Shows: why mutating a collection during iteration causes unpredictable behavior.

To find it: run with items = [2, 4, 6, 8], which should remove all four elements. Print len(items) after the loop; you will get 2 instead of 0. Trace the first iteration: removing items[0] shifts items[1] into position 0, which the loop then skips on its next step.