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

AI-Assisted Programming I: Reading, Reviewing, Trusting

From this lecture on you may use an AI to write code. But before you press enter, you need to be able to judge whether what it gave you is right — and that judgment is what this lecture is about.

By the end of this lecture you should be able to

  • Name the three most common failure modes in AI-generated code and explain why they happen
  • Use a function signature and type hints as the specification you hand to an AI
  • Treat PEP 8 as a review checklist rather than a set of formatting rules
  • Locate problems with a debugger instead of print statements
  • Write an adequate AI usage declaration and state where the academic integrity line falls
Contents

Why now

For seven weeks you were not allowed to use an AI to write code. From today you are.

Before covering how, it is worth being explicit about why the wait — otherwise the rule reads as bureaucracy.

Those seven weeks built two things: a feel for code and the ability to debug. Looking at a fragment and predicting its output. Reading an error and guessing the line. Floating point misled you in L2, [[0]*n]*m caught you in L5, and RecursionError turned up in L7. There is no shortcut past any of that.

Hand those weeks to an AI and you end up with a pile of working code and no judgment at all.

From today, that judgment is the most valuable thing you have. Because an AI will write code that looks entirely reasonable and falls over on some edge case — and it writes so fluently and so confidently that “it looks right” will not save you.

This lecture is not about getting an AI to write more. It is about telling whether what it wrote is correct.

1. How an AI “writes” code

It predicts; it does not verify

A large language model works, at bottom, by predicting the next token from what came before. It has read an enormous amount of code and learned what usually follows what, in context.

The crucial part: it has never run your code and never checked that it is correct. What it emits is whatever most resembles a correct answer.

Most of the time resembling and being coincide — which is why it is useful. The interesting cases are where they come apart.

Three characteristic failures

One: a hallucinated API — calling a function or method that does not exist.

s = "hello"
print(s.reverse())      # strings have no reverse method

The model has seen list.reverse(), and it has seen plenty of reversal contexts, so it “reasonably” infers str.reverse(). But strings are immutable and have no such method.

This kind is relatively easy to catch — it raises immediately.

Two: an unhandled edge case — the main logic is fine, but empty input, a single element, or an out-of-range index was never considered.

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

print(average([1, 2, 3]))     # fine
print(average([]))            # empty list

This kind is far more dangerous, because it behaves perfectly while you are testing and only detonates when real data turns out to contain an empty list.

Three: outdated or simply wrong idioms — the training data contains a great deal of old code.

# the model may hand you Python 2
print "hello"

Or a deprecated library, or a function whose signature has since changed.

2. How to ask

A signature beats a description

You met type hints in L4, where I mentioned a third use for them — they are the specification you hand to an AI. Here is that in full.

Compare two ways of asking:

❌ “write me a function that merges two lists, sorted”

Far too much is unstated. What types? Are the inputs already sorted? What about duplicates? A new list, or modified in place?

✅ Hand it this instead:

def merge_sorted(a: list[int], b: list[int]) -> list[int]:
    """Merge two ascending integer lists into a new ascending list.
    Duplicates are kept. The inputs are not modified.
    """
    ...

Three lines settle what a hundred words of prose leaves open. A signature is an executable specification.

Give context, constraints and examples

A good prompt carries:

  • what you want — the goal, not the steps
  • constraints — which libraries are off limits, performance requirements, style
  • examples — inputs and expected outputs, especially edge cases
Write count_words(text: str) -> dict[str, int] counting word frequency.
Requirements:
- case-insensitive
- strip punctuation from the ends of words
- standard library only

Examples:
count_words("The cat. THE dog") → {"the": 2, "cat": 1, "dog": 1}
count_words("") → {}

Note that empty-string example. Supplying edge cases yourself measurably reduces the second failure mode — you have forced the model to consider it.

One small step at a time

This is L7’s divide-and-conquer applied directly.

Ask for three hundred lines and you cannot verify them. Ask for one function and you can read it, test it, and judge it. Breaking the problem apart is your job, not the AI’s.

3. How to read: PEP 8 as a checklist

Reviewing code requires a standard. Python’s is PEP 8.

PEP 8 has traditionally been taught as “rules you must follow”. Reframe it: it is the checklist you review against. Guido’s insight behind it is the reason:

Code is read far more often than it is written.

That weighs more heavily now than ever — you will spend far more time reading code than writing it.

Indentation and layout

  • four spaces per level, never tabs (L4)
  • lines no longer than 79 characters (many teams relax this to 100 or 120)
  • two blank lines between top-level functions, one between methods in a class

Continuation lines should align:

# good: aligned with the opening bracket
result = some_function(first_argument,
                       second_argument)

# good: one extra level of indentation
result = some_function(
    first_argument,
    second_argument,
)

Breaking around binary operators

Knuth’s traditional advice: break before the operator, so the start of each line shows what operation it performs.

# worse
income = (gross_wages +
          taxable_interest -
          ira_deduction)

# better
income = (gross_wages
          + taxable_interest
          - ira_deduction)

Imports

Imports go at the top of the file, right after any module comment and docstring, in three groups separated by blank lines:

# 1. standard library
import os
import sys

# 2. third party
import numpy as np

# 3. this project
from mypackage import helper

Never write from module import * — you cannot see what names it brings in, and they may quietly shadow your own.

Whitespace: the three that matter most

One: no space just inside brackets

# good
spam(ham[1], {eggs: 2})

# bad
spam( ham[ 1 ], { eggs: 2 } )

Two: no space before a comma, colon or semicolon

# good
if x == 4:
    print(x, y)

# bad
if x == 4 :
    print(x , y)

Three: one space either side of a binary operator — though you may use spacing to show precedence

# good
i = i + 1
x = x*2 - 1              # spacing shows * binds tighter than -
c = (a+b) * (a-b)

# bad
i=i+1
x = x * 2 - 1            # the structure is invisible

Keyword argument equals signs get no surrounding spaces — unless an annotation is present:

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

def g(x: int, y: int = 2) -> int:    # with an annotation, spaces
    ...

Naming

ThingStyleExample
variables, functionssnake_casestudent_count, is_prime
constantsUPPER_CASEMAX_RETRY, PI
classesPascalCaseTriangle, BankAccount
modules, packageslowercaseutils, mypackage
internal use_leading_underscore_helper

And one from L3: never use list, str, sum, type or id as variable names.

Let tools handle format; you handle logic

Formatting should not consume your attention. Install a formatter:

pip install black ruff
black your_file.py       # reformat
ruff check your_file.py  # flag style problems and common mistakes

VS Code can run it on save. Give formatting to the tools and keep your attention for the logic — which is exactly the right division when reviewing AI output too. Do not spend your time on whitespace; look at whether the logic is correct.

4. How to verify

Read the error first

You have met plenty of these by now. Consolidated:

ErrorMeaningTypical cause
SyntaxErrornot valid Pythonmissing colon, unbalanced brackets, full-width punctuation
IndentationErrorindentation is wronginconsistent levels, mixed tabs
NameErrora name was never definedtypo, or used before assignment
TypeErrorwrong typestring plus number, wrong argument count
ValueErrorright type, unacceptable valueint("abc")
IndexErrorindex out of rangea[len(a)]
KeyErrorno such keyd[k] instead of d.get(k)
ZeroDivisionErrordivision by zeroaveraging an empty list
AttributeErrorno such attribute or methodthe usual symptom of a hallucinated API
RecursionErrorrecursion too deepmissing base case

The first reaction to red text should be to read it, not to panic. The last line of a traceback names the error; the line above it says where.

From print to the debugger

Print debugging works, but it has three problems: it changes your code, it shows one thing at a time, and you have to clean it up afterward.

A debugger pauses execution on any line, shows you every variable’s current value, and steps forward one line at a time.

In VS Code:

  1. click in the margin to the left of a line — a red dot appears (a breakpoint)
  2. press F5 to start debugging
  3. execution stops at the breakpoint and the side panel lists every variable
  4. move forward with:
KeyAction
F10run this line (step over calls)
F11run this line (step into calls)
Shift+F11step out of the current function
F5continue to the next breakpoint

Verify with tests, not with “it looks right”

This is the most important sentence in the section.

You cannot judge AI-generated code by whether it reads plausibly. Producing plausible-reading output is precisely what it was trained to do.

The only basis is: feed it inputs and check the outputs — especially at the edges.

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

# test it yourself, edges included
tests = [
    ([1, 2, 3], 2.0),
    ([5], 5.0),
    ([-1, 1], 0.0),
]
for nums, expected in tests:
    got = average(nums)
    print(f"average({nums}) = {got}  {'ok' if got == expected else 'FAIL, expected ' + str(expected)}")

That hand-written check is already worth a great deal. L11 formalizes it with pytest — and the lab platform marks your work by exactly the same mechanism.

An edge-case checklist

Reviewing any code, ask:

  • empty input: empty list, empty string, empty dict
  • a single element: does the logic still hold?
  • duplicates: double-counted, or wrongly removed?
  • zero and negatives: what happens at 0, at -1?
  • very large input: what happens at a thousand times the size?
  • wrong type: what if a string arrives?

5. Workshop: find the problem in four snippets

Each snippet below is something an AI might plausibly hand you. Each has one problem. Find it yourself before reading on.

Snippet one

def remove_all(items: list[int], value: int) -> list[int]:
    """Remove every element equal to value."""
    items.remove_all(value)
    return items

print(remove_all([1, 2, 3, 2, 4], 2))

Snippet two

def max_gap(nums: list[int]) -> int:
    """Return the largest difference between adjacent elements once sorted."""
    s = sorted(nums)
    gaps = [s[i + 1] - s[i] for i in range(len(s) - 1)]
    return max(gaps)

print(max_gap([3, 9, 1, 15]))     # fine
print(max_gap([7]))               # a single element

Snippet three

def dedup(items: list[int]) -> list[int]:
    """Deduplicate, preserving order."""
    result: list[int] = []
    for x in items:
        if x not in result:        # look at this line
            result.append(x)
    return result

print(dedup([3, 1, 3, 5, 1]))     # the answer is right

# but what about at scale?
import time
big = list(range(10000)) * 2
t = time.time()
dedup(big)
print(f"deduplicating 10,000 distinct values took {time.time() - t:.2f} s")

Snippet four

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}))

What was wrong

One: a hallucinated API. Lists have no remove_all. It raises AttributeError. This is the easiest kind to catch — it crashes.

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

print(remove_all([1, 2, 3, 2, 4], 2))

Two: an unhandled edge case. With one element, gaps is empty and max([]) raises ValueError. The core logic is entirely correct; one guard is missing.

def max_gap(nums: list[int]) -> int:
    if len(nums) < 2:
        return 0                   # or raise something meaningful
    s = sorted(nums)
    return max(s[i + 1] - s[i] for i in range(len(s) - 1))

print(max_gap([3, 9, 1, 15]), max_gap([7]), max_gap([]))

Three: a complexity trap. The answer is correct, but x not in result searches a list, scanning from the front every time. The more data, the slower — and it degrades quadratically (L5 flagged the cost of list search).

A set fixes it (L6):

def dedup(items: list[int]) -> list[int]:
    seen: set[int] = set()
    result: list[int] = []
    for x in items:
        if x not in seen:          # a set lookup, effectively one step
            seen.add(x)
            result.append(x)
    return result

import time
big = list(range(10000)) * 2
t = time.time()
dedup(big)
print(f"with a set: {time.time() - t:.4f} s")

Four: a style disaster. The logic is right, but:

  • the name f says nothing
  • l and D are meaningless, and l is nearly indistinguishable from 1 (PEP 8 forbids it explicitly)
  • R breaks the naming convention
  • no type hints, no docstring
  • the 0 in range(0, len(l)) is redundant, and it should iterate the elements directly
  • three levels of nested if collapse into one condition

Rewritten:

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

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

Nine lines down to three, and you understand it on one reading.

6. Who does what

This course used to have a section on programming discipline. Today it becomes a question of division of labor.

StageWhose jobWhy
understanding and decomposing the problemyoursthe AI does not know what you actually want
designing modules and interfacesyoursthis determines the shape of the whole program
implementing one specified functionthe AI cana well-specified, checkable task
boilerplate, format conversionthe AI canmechanical, repetitive, easy to verify
judging performance and complexityyoursAIs readily produce solutions that run and crawl
designing test casesyours (the AI can add more)edge cases need your judgment
deciding whether to accept ityoursyou answer for every line you submit

A few principles:

Think before you write. Unchanged advice — except that “think” and “write” can now be done by different parties.

Do not use idioms you cannot vouch for. This one has doubled in weight. When an AI hands you an unfamiliar function or construction, check the documentation that it exists and does what you assume before using it.

Make it work before making it fast — while still recognizing when a design will collapse at scale. Snippet three is the example.

7. Academic integrity

The usage declaration

From this week, every submission carries a short declaration covering three things:

  1. what you used — which tool, which model
  2. what it produced — down to the function or block
  3. how you verified it — the tests you ran, the edge cases you tried

An adequate example:

AI usage declaration:
Used DeepSeek for the regular expression inside parse_line().
I gave it three sample inputs and their expected outputs.
Its first version did not handle trailing whitespace; I added .strip().
Also tested with an empty string and a line of only separators; both fine.
Everything else is my own.

The declaration costs you nothing. Omitting it does — because omitting it means you never thought about the division of labor.

Where the line is

Handled as academic misconduct:

  • using an AI to generate code during Level 0 (weeks 1–7)
  • submitting code, at any stage, that you cannot talk through
  • being unable to explain your own implementation at an oral check

The test is one sentence: can you answer for every line you hand in?

Summary

  • An LLM predicts the next token; it does not verify correctness. It emits whatever most resembles a right answer
  • Three characteristic failures: hallucinated APIs (loud, easy to catch), unhandled edge cases (dangerous), outdated idioms
  • And a fourth, quietest: correct but with catastrophic complexity
  • Prompt with a signature, type hints, and edge-case examples — far more precise than prose
  • Ask one small step at a time. Code you cannot read is code you have not verified
  • PEP 8 is a review checklist: indentation, naming, whitespace, imports. Give formatting to black/ruff
  • Move from print to a debugger: breakpoints, stepping, variable panel, call stack
  • Judge code by tests, never by whether it reads plausibly
  • The edge list: empty, one, duplicates, zero and negatives, very large, wrong type
  • You own understanding, decomposition, design, performance judgment and acceptance. The AI implements what you have specified
  • Attach an AI usage declaration every time. You answer for every line

Exercises

  1. Find the two problems in this snippet (one raises, one does not) and fix them:
    def most_common(words: list[str]) -> str:
        counts = {}
        for w in words:
            counts[w] += 1
        return max(counts, key=counts.get)
  2. Write a specification for an AI — signature, docstring and at least three examples including edge cases — for: “convert a list of scores into a list of letter grades”.
  3. Get an AI to write a function that decides whether a string is a valid IPv4 address. Then:
    • without looking at its code, write at least six test cases of your own, including edge cases
    • run your cases against it
    • record whether it failed, and where
  4. Hand an AI one of your recursive functions from L7 and ask it to rewrite it as a loop. Check that the rewrite is equivalent, paying particular attention to edge cases.
  5. Install ruff and run it over your code from Labs 1 to 7. Count the problems it reports. Pick three you do not understand and find out why they are flagged.
  6. Read the section “A Foolish Consistency is the Hobgoblin of Little Minds” in PEP 8 and summarize its point in one sentence.

The lab problems are in Lab 8. From this week the AI policy moves to Level 1.