CS1602Introduction to Computation
Lecture 8Part 3 Problem Solving with AIAI Level 1

AI-Assisted Programming I: Specification, Review and Verification

A sample run is evidence, not an acceptance decision. Learn to specify a function, review its implementation, build independent checks, investigate a failure and document the code you choose to use.

By the end of this lecture you should be able to

  • Separate generated text from tool execution and verification
  • State a function's interface, behavioral contract and edge cases
  • Review readability with PEP 8 without mistaking style for correctness
  • Design tests and investigate a counterexample with print or a debugger
  • Check repeated work and input scale before accepting an implementation
  • Document AI assistance and distinguish this week's lab from Solo assignment 1
Contents

1. Evidence before acceptance

L07 asked what a recursive function promises, how its calls make progress and whether its results are correct. Those questions are just as useful when the implementation comes from a classmate, a website or an AI assistant.

Suppose the supplied code is short, readable and passes the example. What is still unknown? Perhaps it modifies an input that should remain unchanged. Perhaps the example omits an empty input. Perhaps the result is right but the amount of work is unsuitable for the actual data.

The candidates in this lecture are constructed teaching examples, not verbatim transcripts from a particular model. Their purpose is to make the review process observable.

Generated text is not an execution log

A token is a unit used to process text: it may represent a word, part of a word or punctuation, rather than exactly one character. A language model generates subsequent tokens from its context. A code block and a convincing explanation do not, by themselves, show that the code was executed.

An assistant with tools may also search documentation, run programs and execute tests. Inspect what was actually done:

EvidenceWhat it supportsWhat it does not establish
A candidate and an explanationOne proposed implementation and rationaleThat it ran or meets every requirement
A recorded input and outputThe behavior of that executionCorrectness for all inputs
Test cases and their resultsWhether those checks passedComplete coverage of possible defects

Acceptance needs the requirement, the implementation and the available evidence together. The tool can contribute to the work; the person submitting the code must still understand the adopted implementation.

Three useful failure checks

Check the API. A string has no reverse() method:

s = "hello"
print(s.reverse())

The result is an AttributeError. The existence of list.reverse() does not imply a corresponding string method. Check the documented receiver type, arguments, result and side effects.

Slicing produces a reversed string without changing the original:

s = "hello"
print(s[::-1])
print(s)

Check the input contract. This implementation handles a nonempty list, but division by zero occurs on an empty one:

def average(nums: list[float]) -> float:
    return sum(nums) / len(nums)

print(average([1, 2, 3]))
print(average([]))

If the contract excludes empty lists, that input is outside its domain. If the task requires empty input to be accepted, specify the behavior before changing the function. The candidate’s current behavior is not a substitute for that decision.

Check the runtime. Syntax and library interfaces depend on versions. This is Python 2 syntax, not valid code for the course’s Python 3.13 environment:

# Python 2 syntax, not valid in the course's Python 3 environment.
print "hello"

The example illustrates a compatibility check; it is not a claim that present-day models routinely produce Python 2.

After these checks, there are still performance and readability questions. A program can return the right answer and remain expensive to run or difficult to review.

2. Specify the function, not just its name

Interface and behavior are different

“Merge two lists, sorted” leaves several decisions open. Are the inputs already sorted? Should duplicates survive? May the function change either input?

A useful starting point is an interface plus a behavioral contract:

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Merge two ascending integer lists into a new ascending list.
    Keep duplicates. Do not modify either input.
    """
    ...

The annotation describes input and result types. The docstring states ordering, multiplicity and mutation requirements. This is a specification excerpt, not a completed implementation: ... does not perform the merge.

Python does not enforce type hints automatically. This call still concatenates strings:

def twice(x: int) -> int:
    return x + x

print(twice("ab"))

A type checker or an editor can use the annotation to flag a problem. The annotation alone does not reject the call or verify the result’s behavior.

Mutation is part of the contract

Predict both printed lists:

def without_two(items):
    result = items
    result.remove(2)
    return result

values = [1, 2, 3, 2]
print(without_two(values))
print(values)

Both lines print [1, 3, 2]. Assignment did not copy the list, and remove() removed only the first match.

For a different contract—remove all matches, return a new list and leave the input unchanged—a comprehension is suitable:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
print(remove_all(values, 2))
print(values)

Check both the result and the input:

def remove_all(items: list[int], value: int) -> list[int]:
    return [x for x in items if x != value]

values = [1, 2, 3, 2]
before = values.copy()
result = remove_all(values, 2)
print(result == [1, 3])
print(values == before)

Give enough information to review the answer

A checkable request normally includes:

  • a task, signature and input domain;
  • ordering, duplicates, result type and allowed side effects;
  • the Python version, permitted libraries and known syntax;
  • normal and boundary examples with expected results;
  • relevant input sizes and performance requirements.

For example:

Implement count_words(text: str) -> dict[str, int] in Python 3.13.
Split on whitespace and ignore case.
Strip only . , ! ? ; : from the two ends of each word.
Do not count a token that becomes empty after stripping.
Use only syntax covered in Lectures 1–7.

Examples:
"The cat. THE dog" -> {"the": 2, "cat": 1, "dog": 1}
"" -> {}
"... !" -> {}

This is not a universal prompt template. The punctuation rule, for instance, must match the actual task. Boundary examples make requirements explicit; they do not guarantee an error-free response.

Request small, reviewable changes

A function with a clear contract is easier to inspect than a large, loosely defined program. Decomposition connects directly to L07: isolate a task, implement it, check it and then move on.

For a failure report, record enough to reproduce it:

Function: max_gap
Input: [7]
Expected result: 0, as specified for fewer than two elements
Actual result: ValueError from max([])
Runtime: Python 3.13
Relevant code: the construction of gaps and the final max(gaps)

“Wrong, try again” discards the useful evidence. An assistant’s diagnosis also needs checking; it is not authoritative just because it sounds plausible.

3. Readability as part of review

Recover the behavior before refactoring

This function works on the example, but its names and nesting hide its purpose:

def f(l, D):
    R = []
    for i in range(0, len(l)):
        if l[i] in D:
            if D[l[i]] > 0:
                R.append(l[i])
    return R

print(f(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))

Its task is to retain keys whose counts are positive, keeping input order and repeated keys. Naming that behavior makes review easier:

def filter_positive(
    keys: list[str], counts: dict[str, int]
) -> list[str]:
    """Keep present keys with positive counts, in input order."""
    return [k for k in keys if counts.get(k, 0) > 0]

print(filter_positive(["a", "b", "c"], {"a": 1, "b": 0, "c": 5}))
print(filter_positive(["a", "x", "a"], {"a": 2}))

The type hints and docstring clarify this interface; their absence was not automatically a PEP 8 violation. Shorter code is not enough: check that the refactoring preserves the intended behavior.

PEP 8: conventions, not a proof

PEP 8 discusses readability and consistency. It is a style guide, not a guarantee of functional correctness.

TopicCommon convention
IndentationFour spaces per level; do not mix tabs and spaces
Line lengthNormally 79 for code, 72 for comments/docstrings; a team may agree on another limit
Blank linesTwo between top-level function/class definitions; one between class methods
Functions and variablessnake_case, such as student_count
ConstantsUPPER_CASE, such as MAX_RETRY
ClassesCapWords, such as BankAccount; class syntax comes in L09
Modules and packagesShort lowercase names, such as utils or mypackage
Internal useA leading underscore, such as _helper; this is not access enforcement
Confusing namesAvoid single-letter l, O and I
Built-in namesAvoid shadowing list, str, sum or type when their built-in meaning is needed

Continuation lines may align with an opening delimiter or use a hanging indent:

result = some_function(first_argument,
                       second_argument)

result = some_function(
    first_argument,
    second_argument,
)

These are formatting excerpts, not standalone runnable programs. Either side of a binary operator can be a line-break point if the local style is consistent; PEP 8 suggests breaking before the operator in new code:

income = (gross_wages
          + taxable_interest
          - ira_deduction)

Avoid extra whitespace inside brackets and before commas or colons. Keep spaces around assignment and comparison operators. For mixed-precedence expressions, the guide permits judgment about spacing to expose grouping. Spacing does not change precedence, and a style issue need not be a syntax error.

# Recommended layout.
spam(ham[1], {eggs: 2})
if x == 4:
    print(x, y)

# Legal in suitable contexts, but not the recommended layout.
spam( ham[ 1 ], { eggs: 2 } )

Unannotated parameter defaults and keyword arguments have no spaces around =. Annotated defaults do:

def f(x, y=2):
    ...


def g(x: int, y: int = 2) -> int:
    ...

Three tools, three kinds of work

WorkExampleWhat it checks or changes
Formattingruff format file.py, or BlackLayout and whitespace
Lintingruff check file.pyEnabled static rules, such as unused imports or undefined names
Behavioral testingExecute chosen cases and compare resultsSpecific requirements

Ruff only checks the selected rules. Its defaults are not “all of PEP 8”, and an empty diagnostic report does not certify the program’s logic. A formatter will not decide what averaging an empty list should mean.

python3 -m pip install ruff
ruff check file.py
ruff format --check file.py     # check without rewriting

The VS Code Ruff extension is another route. If pip reports an externally managed environment, do not force an administrator-level installation; use the course’s Python installation or the editor extension. L10 covers virtual environments. Black is an alternative formatter, not an additional mandatory installation.

4. Tests, counterexamples and debugging

Expected results come from the contract

For max_gap, specify an integer list, the largest adjacent gap after sorting, a result of zero for fewer than two elements and unchanged input.

InputExpectedPurpose
[3, 9, 1, 15]6An unsorted ordinary input
[]0Empty input
[7]0One element
[2, 2, 2]0Repeated values
[-3, -1, -10]7Negative values

The candidate omits the short-input case:

def max_gap(nums: list[int]) -> int:
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

print(max_gap([7]))

The list of gaps is empty, so max() raises ValueError. After a correction, recheck the failure and previously working cases:

def max_gap(nums: list[int]) -> int:
    if len(nums) < 2:
        return 0
    ordered = sorted(nums)
    gaps = [ordered[i + 1] - ordered[i]
            for i in range(len(ordered) - 1)]
    return max(gaps)

for nums, expected in [([3, 9, 1, 15], 6), ([7], 0), ([], 0)]:
    before = nums.copy()
    got = max_gap(nums)
    print(got == expected, nums == before)

This is a regression check. A finite suite can expose defects, but generally does not prove correctness on every input. Read the bounds, conditions and state changes as well.

Useful review dimensions include empty input, one element, repeated values, zero and negatives, scale, output order and mutation. Wrong-type handling is contract-dependent: do not silently require a function specified for integer lists to accept every possible object.

Read the traceback as a record

ExceptionTypical meaning or example
SyntaxErrorInvalid syntax, such as a missing colon
IndentationErrorInvalid indentation structure
NameErrorA name has not been defined
TypeErrorAn operation or call rejects the type/argument form
ValueErrorA value violates the interface requirement, such as int("abc")
IndexErrorA sequence index is outside its range
KeyErrorDirect indexing requests a missing dictionary key
ZeroDivisionErrorA divisor is zero
AttributeErrorAn attribute or method is unavailable on the object
RecursionErrorExecution exceeds the interpreter’s recursion-depth limit

A traceback contains call locations and the failing statement; its final line normally names the exception and message. Source lines and caret markers may intervene, so “the second-to-last line is the error location” is not a reliable rule. A recursion-depth failure can come from faulty recursion or a valid but too-deep input. Exception-handling syntax comes in L11.

Observe the state of a failing case

The following candidate is meant to return the longest run of ones from a list containing only zero and one; empty input returns zero:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return best

print(longest_run([1, 1, 0]))
print(longest_run([1, 1, 0, 1, 1, 1]))

Both calls return 2, but the second should return 3. Just before return, best=2 and current=3. The final run never met a zero, so it was not included in best.

A temporary print can expose these values. Alternatively, pause on the return statement in VS Code:

  1. Enable the Python and Python Debugger extensions and select the course interpreter.
  2. Click the gutter beside the return statement to set a breakpoint.
  3. Start from Run and Debug; execution pauses before that statement runs.
  4. Inspect the selected frame’s variables, or add best and current to Watch.
ActionCommon shortcutBehavior
ContinueF5Resume until a breakpoint or completion
Step OverF10Execute the line without entering the called function
Step IntoF11Enter a function called on the line
Step OutShift+F11Finish the current function and return to the caller

The operating system, keyboard and custom bindings can change shortcuts; some keyboards need Fn. The toolbar action names are another way to operate the debugger. Watch evaluates in the selected context when execution pauses, not continuously during execution.

For this contract, include the unfinished run when returning:

def longest_run(bits: list[int]) -> int:
    current = 0
    best = 0
    for bit in bits:
        if bit == 1:
            current += 1
        else:
            best = max(best, current)
            current = 0
    return max(best, current)

for bits in [[], [0], [1], [1, 1, 0], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

Select a recursive frame

When L07’s factorial(3) has entered factorial(1), three calls are active with local n values 1, 2 and 3. Selecting the outer frame shows its own n=3; the inner call has not overwritten it.

Print is convenient for a few values or a batch log. A debugger is convenient for pausing, stepping and selecting frames. Both remain useful; neither is a universal replacement for the other.

5. Correct answers at the required scale

This deduplication preserves first-occurrence order:

def dedup(items: list[int]) -> list[int]:
    result = []
    for x in items:
        if x not in result:
            result.append(x)
    return result

print(dedup([3, 1, 3, 5, 1]))

Reading the membership check reveals repeated searches through a growing list. An explicit search lets us count how many existing elements are examined:

def dedup_counted(items: list[int]) -> tuple[list[int], int]:
    result = []
    comparisons = 0
    for x in items:
        found = False
        for old in result:
            comparisons += 1
            if old == x:
                found = True
                break
        if not found:
            result.append(x)
    return result, comparisons

for n in [10, 100, 1000]:
    result, checks = dedup_counted(list(range(n)) * 2)
    print(n, checks)

For this particular input—n distinct elements appearing twice in the same order—the explicit comparison count is n². That is not a measurement of CPython machine instructions, nor a claim that all distributions use exactly n² checks.

A set can record seen integers while a list retains result order:

def dedup(items: list[int]) -> list[int]:
    seen = set()
    result = []
    for x in items:
        if x not in seen:
            seen.add(x)
            result.append(x)
    return result

for items in [[], [3, 1, 3, 5, 1], [-1, 0, -1]]:
    before = items.copy()
    print(dedup(items), items == before)

Set lookup has a favorable average cost; it is not guaranteed to be a single step. This interface uses hashable integers, not arbitrary nested lists.

A timing comparison must use comparable data, environments and measurement methods, and first establish equivalent results. Hardware, runtimes and other work affect elapsed seconds. One observed speedup is not a universal ratio. L11 develops the time/space analysis formally.

6. Review, acceptance and disclosure

Four candidates, different questions

The lecture has used four review tasks:

  1. items.remove_all(value): determine API availability through documentation and execution.
  2. max_gap: expose an omitted boundary with a short input.
  3. dedup: identify repeated list search and check the amount of work.
  4. f(l,D): recover the behavior, improve readability and check equivalence.

Do not assume that every candidate has exactly one defect. Do not classify behavior outside the stated domain as a defect without first checking the requirement.

A precise IPv4 exercise

Lab08 uses is_valid_ipv4(s: str) -> bool. Its string inputs follow this strict dotted-decimal contract:

  • exactly four parts separated by .;
  • ASCII digits 0 through 9 only, each value in 0–255;
  • the single digit 0 is allowed; other leading-zero forms are not;
  • no empty parts, signs or surrounding/internal whitespace.

Write independent tests before inspecting a candidate. Valid cases can include 0.0.0.0, 192.168.1.1 and 255.255.255.255. Invalid cases should cover part count, range, leading zeros and character restrictions. The full corrected lab solution is not supplied here.

A documented conversion may accept more than the task permits, and a familiar character predicate may be broader than expected:

for s in ["01", "+1", " 1", "1"]:
    print(repr(s), int(s))

for s in ["1", "1", "²"]:
    print(repr(s), s.isdigit())

The ASCII restriction is explicit in this exercise. Python’s ipaddress documentation states the four-part and leading-zero requirements but does not spell out that character restriction on the referenced page. Keep documented claims, observed behavior and the task’s own contract distinct.

Division of work and safe use

StageWork to retain
Understand and decomposeDecide goals, inputs, outputs and interfaces
ImplementWrite independently or use tools as the policy permits
ReviewUnderstand state, unfamiliar APIs and side effects
TestDesign checks from the contract; tools may add suggestions
CorrectKeep the counterexample, original candidate and change scope
AcceptExplain the evidence and take responsibility for the submitted code

Work out the task before implementing it. Check unfamiliar constructions against documentation and the runtime. Make the function work, then decide what the required input sizes demand; avoiding premature optimization does not excuse an obviously unsuitable design.

Remove personal information, tokens, passwords and keys before sending material to an external tool. Do not run unfamiliar file operations, installations or network requests without inspecting them. Keep teaching failures small and within an environment you control.

An AI usage declaration

The existing course policy permits AI assistance for this week’s lab with disclosure of the tool, generated scope and verification. This is a format example, not a real usage log:

AI usage declaration:
Tool and model: record what was actually used; say if the model is unknown.
Generated scope: for example, the initial remove_all() implementation.
Manual changes: identify the functions, conditions or blocks changed.
Verification: actual inputs, expected and observed results; input preservation.
Independent work: identify the parts completed without AI.

“An AI said it was correct” is not a test record. If an online tool was unavailable and the instructor’s candidate was used, disclose that source rather than inventing a conversation. If no AI was used, state that explicitly.

Solo assignment 1 remains Level 0, covers ordinary and adjacent-peg Hanoi and is worth 5% of the semester grade. Its deadline is on the submission page. This week’s Level 1 does not change that assignment’s rule. Do not send its implementation to an AI for generation or rewriting; this lecture uses separate classroom examples such as factorial.

7. Reference material and legacy exercises

Imports, comments and layout details

Keep these as reference checks:

  • Put imports after the module docstring; group standard library, third-party and project imports with blank lines. Avoid wildcard imports whose names may shadow others.
  • Prefer implicit continuation inside brackets; avoid trailing whitespace and multiple statements crowded onto a line.
  • A one-element tuple needs a comma; use trailing commas in multiline constructs according to the guide and project conventions.
  • Comments should explain reasons and stay synchronized with the code. Docstrings state purpose, parameters and results.
  • Python 3 source defaults to UTF-8. Keep quoting consistent without treating one quote style as uniquely valid.
  • Class, inheritance, public-interface and exception-naming conventions become useful as later lectures introduce those constructs; they are not prerequisites now.

This import grouping is a formatting excerpt; it does not assume that the third-party and project modules are installed:

import os
import sys

import numpy as np

from mypackage import helper

Consult the full PEP 8 for details. The Zen of Python is available through:

import this

Tim Peters wrote these principles. Readability and explicitness describe design choices, not a line-by-line correctness certificate.

Leap years: equivalent implementations

For positive Gregorian years, the rule is divisibility by four but not one hundred, or divisibility by four hundred. These three structures express that rule:

def leap_separate(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    if year % 400 == 0:
        return True
    return False


def leap_branches(year: int) -> bool:
    if year % 4 == 0 and year % 100 != 0:
        return True
    elif year % 400 == 0:
        return True
    else:
        return False


def leap_boolean(year: int) -> bool:
    return (year % 4 == 0 and year % 100 != 0
            or year % 400 == 0)

for year in [1900, 2000, 2024, 2100]:
    print(year, leap_separate(year), leap_branches(year), leap_boolean(year))

Primes: domain and stopping bounds

A prime is an integer greater than one with exactly two positive divisors. Both legacy versions omitted the n<2 guard. Normal positive samples did not expose it.

def prime_slow(n: int) -> bool:
    if n < 2:
        return False
    for divisor in range(2, n):
        if n % divisor == 0:
            return False
    return True


def prime_fast(n: int) -> bool:
    if n < 2:
        return False
    divisor = 2
    while divisor * divisor <= n:
        if n % divisor == 0:
            return False
        divisor += 1
    return True

for n in [-1, 0, 1, 2, 3, 4, 9, 97]:
    print(n, prime_slow(n), prime_fast(n))

A factor above the square root is paired with one below it. Include the equality boundary for perfect squares; integer multiplication avoids using a floating approximation for that decision.

Digit squares and happy numbers

For nonnegative integers, digit-square sums can be computed by arithmetic iteration, recursion or string traversal:

def squares_loop(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def squares_recursive(n: int) -> int:
    if n < 10:
        return n * n
    return (n % 10) ** 2 + squares_recursive(n // 10)


def squares_text(n: int) -> int:
    total = 0
    for ch in str(n):
        total += int(ch) ** 2
    return total

for n in [0, 2, 9, 19, 29]:
    print(n, squares_loop(n), squares_recursive(n), squares_text(n))

The old recursive base case returned n: it gave 2 rather than 4 for input 2, although the example 19 happened to work. Small boundary checks matter. Recursion also has a depth limit.

A positive integer is happy if repeated digit-square replacement reaches one. An already visited state means that the deterministic process will repeat:

def sum_digit_squares(n: int) -> int:
    total = 0
    while n > 0:
        total += (n % 10) ** 2
        n //= 10
    return total


def is_happy(n: int) -> bool:
    seen = set()
    while n != 1:
        if n in seen:
            return False
        seen.add(n)
        n = sum_digit_squares(n)
    return True

print(is_happy(19))
print(is_happy(2))

The set records which states occurred, not their multiplicities. That is enough for cycle detection. Zero is outside the positive-input definition.

Runs: consecutive positions matter

The legacy nested-while implementation also finds the longest run of ones. The outer index skips a run already traversed by the inner loop, so nesting alone does not imply quadratic work.

def longest_run(bits: list[int]) -> int:
    best = 0
    i = 0
    while i < len(bits):
        if bits[i] == 1:
            j = i
            while j < len(bits) and bits[j] == 1:
                j += 1
            best = max(best, j - i)
            i = j
        else:
            i += 1
    return best

for bits in [[], [0], [1], [1, 1, 0, 1, 1, 1]]:
    print(bits, longest_run(bits))

A continuous increasing run compares adjacent positions and requires strict increase. Equal values end the current run. It cannot skip a value to form an arbitrary subsequence:

def longest_increasing_run(nums: list[int]) -> int:
    if not nums:
        return 0
    current = 1
    best = 1
    for i in range(1, len(nums)):
        if nums[i] > nums[i - 1]:
            current += 1
        else:
            current = 1
        best = max(best, current)
    return best

for nums in [[], [1, 3, 5, 4, 7], [2, 2, 2], [1]]:
    print(nums, longest_increasing_run(nums))

Anagrams require multiplicities

For lowercase ASCII strings, matching character counts establishes an anagram. Counting and sorting are two possible implementations:

def char_counts(s: str) -> dict[str, int]:
    counts = {}
    for ch in s:
        counts[ch] = counts.get(ch, 0) + 1
    return counts


def anagram_counts(s: str, t: str) -> bool:
    return char_counts(s) == char_counts(t)


def anagram_sorted(s: str, t: str) -> bool:
    return sorted(s) == sorted(t)

for s, t in [("anagram", "nagaram"), ("rat", "car"), ("aab", "abb"), ("", "")]:
    print(s, t, anagram_counts(s, t), anagram_sorted(s, t))

set("aab") equals set("abb"), but the multiplicities differ. Sets do not preserve the information this task needs.

Simplification and development habits

For an actual bool parameter, if flag is straightforward. For a general object, truthiness is not always equivalent to flag == True. Similarly, L05’s += and + examples show that a shorter spelling can change mutation behavior.

Retain useful habits from the old lecture: plan the task and intermediate state, separate functions by interfaces, test a completed small piece, reuse computed results, name constants, check unfamiliar APIs and preserve failures. Function length, loop depth or brevity do not prove correctness; append() also has appropriate uses.

Summary

Acceptance combines a contract, code review, documentation and runtime checks, targeted tests, mutation checks and scale considerations. A tool run is evidence; a finite test suite is not generally a proof for every input.

Formatters, linters, print and debuggers solve different problems. Preserve the original candidate, failures, modifications and regression results so the acceptance decision can be explained.

L09 asks how the data and operations of similar records can be organized together using classes.

Exercises

  1. Investigate the candidate below on both nonempty and empty input. Revised contract: return an empty string for empty input; when frequencies tie, return the first word encountered. Correct it and test that contract.
    def most_common(words: list[str]) -> str:
        counts = {}
        for word in words:
            counts[word] += 1
        return max(counts, key=counts.get)
  2. Specify a function converting scores into letter grades. Include a signature, behavior and at least three examples. Decide score bounds, grade thresholds, empty-input behavior and mutation.
  3. Write at least six tests from this lecture’s IPv4 contract before examining an AI or instructor-provided candidate. Record expected/observed results and a correction.
  4. Compare recursive and iterative versions of the classroom factorial() function, including n=0. AI assistance may follow this week’s policy; do not ask it to rewrite Solo assignment 1.
  5. Run Ruff on your own Labs 1–7 code and explain three unfamiliar diagnostics. If fewer exist, use Lab08’s practice file rather than inventing warnings.
  6. Read PEP 8’s “A Foolish Consistency is the Hobgoblin of Little Minds” and summarize how local consistency and readability differ from mechanically applying rules.

See Lab08 for the A/B/C practical work. Its package and text cards are prepared locally but not synchronized to the server. Use HW3’s week-eight entries after publication and follow the lab’s current saving/submission instructions. The policy is on the AI policy page.