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:
| Evidence | What it supports | What it does not establish |
|---|---|---|
| A candidate and an explanation | One proposed implementation and rationale | That it ran or meets every requirement |
| A recorded input and output | The behavior of that execution | Correctness for all inputs |
| Test cases and their results | Whether those checks passed | Complete 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.
| Topic | Common convention |
|---|---|
| Indentation | Four spaces per level; do not mix tabs and spaces |
| Line length | Normally 79 for code, 72 for comments/docstrings; a team may agree on another limit |
| Blank lines | Two between top-level function/class definitions; one between class methods |
| Functions and variables | snake_case, such as student_count |
| Constants | UPPER_CASE, such as MAX_RETRY |
| Classes | CapWords, such as BankAccount; class syntax comes in L09 |
| Modules and packages | Short lowercase names, such as utils or mypackage |
| Internal use | A leading underscore, such as _helper; this is not access enforcement |
| Confusing names | Avoid single-letter l, O and I |
| Built-in names | Avoid 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
| Work | Example | What it checks or changes |
|---|---|---|
| Formatting | ruff format file.py, or Black | Layout and whitespace |
| Linting | ruff check file.py | Enabled static rules, such as unused imports or undefined names |
| Behavioral testing | Execute chosen cases and compare results | Specific 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.
| Input | Expected | Purpose |
|---|---|---|
[3, 9, 1, 15] | 6 | An unsorted ordinary input |
[] | 0 | Empty input |
[7] | 0 | One element |
[2, 2, 2] | 0 | Repeated values |
[-3, -1, -10] | 7 | Negative 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
| Exception | Typical meaning or example |
|---|---|
SyntaxError | Invalid syntax, such as a missing colon |
IndentationError | Invalid indentation structure |
NameError | A name has not been defined |
TypeError | An operation or call rejects the type/argument form |
ValueError | A value violates the interface requirement, such as int("abc") |
IndexError | A sequence index is outside its range |
KeyError | Direct indexing requests a missing dictionary key |
ZeroDivisionError | A divisor is zero |
AttributeError | An attribute or method is unavailable on the object |
RecursionError | Execution 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:
- Enable the Python and Python Debugger extensions and select the course interpreter.
- Click the gutter beside the return statement to set a breakpoint.
- Start from Run and Debug; execution pauses before that statement runs.
- Inspect the selected frame’s variables, or add
bestandcurrentto Watch.
| Action | Common shortcut | Behavior |
|---|---|---|
| Continue | F5 | Resume until a breakpoint or completion |
| Step Over | F10 | Execute the line without entering the called function |
| Step Into | F11 | Enter a function called on the line |
| Step Out | Shift+F11 | Finish 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:
items.remove_all(value): determine API availability through documentation and execution.max_gap: expose an omitted boundary with a short input.dedup: identify repeated list search and check the amount of work.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
0through9only, each value in 0–255; - the single digit
0is 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
| Stage | Work to retain |
|---|---|
| Understand and decompose | Decide goals, inputs, outputs and interfaces |
| Implement | Write independently or use tools as the policy permits |
| Review | Understand state, unfamiliar APIs and side effects |
| Test | Design checks from the contract; tools may add suggestions |
| Correct | Keep the counterexample, original candidate and change scope |
| Accept | Explain 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
- 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) - 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.
- 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.
- 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. - 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.
- 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.