CS1602Introduction to Computation
Lecture 15Part 6 Pythonic Python and Modern PracticeAI Level 2

AI-Assisted Programming II: From Prompt to Project

L8 was about reviewing a function an AI wrote for you. This lecture is about how an AI participates in a whole project — and where it will reliably let you down.

By the end of this lecture you should be able to

  • Break a one-sentence requirement into modules and hand them over interface-first
  • Name what an AI does well and badly at project scale
  • Use an AI for tests, review and documentation while keeping a human in the loop
  • Agree how a team will use AI
  • Judge when you must write it yourself
Contents

How this differs from L8

L8 worked at the scale of one function: an AI hands you some code, and you decide whether it is right.

This lecture jumps to the scale of a project. When there are hundreds or thousands of lines across several modules, written by two people, the questions change:

L8’s questionThis lecture’s question
is this code correctis this design correct
does this function handle its edgesare these module interfaces sensible
how do I verify a return valuehow do I verify a system
who writes this line, me or the AIwho owns which layer

The team project you are working on (Lab 14’s simplified NumPy) sits at exactly this scale.

From this week the AI policy is Level 2: no restrictions. But opening up is not the same as handing over your judgment.


1. From functions to projects

Decompose first, then ask

Suppose the requirement is one sentence: “build a simplified NumPy.”

Hand that straight to an AI and you get hundreds of lines — more than you can read, and therefore more than you can verify. As L8 put it: code you cannot read is code you have not verified.

The first step is decomposition, by you:

Array class
├── data storage      ← the internal representation, the biggest decision
├── shape management  ← shape / ndim / size / reshape / transpose
├── indexing          ← __getitem__ / __setitem__
├── elementwise ops   ← __add__ / __sub__ / __mul__ / __truediv__
├── reductions        ← sum / mean / max / min (with axis)
└── representation    ← __repr__ / __eq__

module-level functions
├── construction      ← zeros / ones / arange
└── matrix product    ← dot

This is your work, not the AI’s. The decomposition determines the program’s shape, and an AI cannot know your trade-offs — how many dimensions do you need? does performance matter? will you support broadcasting?

Once decomposed, you have a set of small tasks you can ask about independently.

Interface first

Having decomposed, do not describe the behavior — write the signature:

class Array:
    def __init__(self, data: list) -> None:
        """Construct from a nested list. `data` must be regular (equal-length rows),
        otherwise raise ValueError.
        Stored internally as a flat list in self._data plus a self._shape tuple.
        """

    def reshape(self, *shape: int) -> "Array":
        """Return a new Array with the given shape, in row-major order.
        The element count must match, otherwise raise ValueError.
        Does not modify this object.

        >>> Array([[1,2],[3,4]]).reshape(4).shape
        (4,)
        """

Those lines settle the input types, the output type, the error conditions, the internal representation, whether it mutates, and one example. An AI given this can barely misunderstand it.

Compare that with “write me a reshape” — now it has to guess the representation, guess whether to mutate, guess what to do on a mismatch. When it guesses wrong you start again.

This is the payoff of type hints’ third use from L4 and of L8’s “a signature is an executable specification”.

Supply the context

At project scale, an AI needs to know what you already have.

An effective habit is to attach, with each question:

  • the relevant existing code — not everything, just what this task touches
  • your conventions: naming, error handling, internal representation
  • the existing tests, so it knows what “correct” means
Here is how Array constructs and stores its data:

class Array:
    def __init__(self, data):
        self._data = [...]      # flat list, row-major
        self._shape = (...)     # shape tuple

Please implement transpose(), such that:
- it returns a new object, leaving this one alone
- only 1-D and 2-D need supporting
- 1-D returns a copy of itself
- it uses our existing _data / _shape representation

Existing tests that must pass:
    assert Array([[1,2,3],[4,5,6]]).transpose().shape == (3, 2)
    assert Array([1,2,3]).transpose().shape == (3,)

One small step, verified each time

This is L7’s divide and conquer applied to collaboration:

  1. ask for one function
  2. read it — if you do not follow it, ask for an explanation or a different approach
  3. test it — with cases you prepared
  4. only then ask for the next one

2. What an AI does well and badly

Well

TaskWhy it suits
boilerplatemechanical, repetitive, patterned
format conversioninput and output both explicit
writing test casesit enumerates common situations well (edges are on you)
writing docstringsthe code is right there; describing it is its strength
explaining errorsit has seen enormous numbers of them
refactoringclear instructions like “rewrite this as a comprehension”
looking up API usagefaster than the docs (but verify)

Badly

One: judging performance and complexity.

This is the category to watch. An AI tends to produce something that runs, not something that works at your scale.

# a dot implementation an AI might well hand you
def dot_naive(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
    n, m, p = len(a), len(b), len(b[0])
    result = []
    for i in range(n):
        row = []
        for j in range(p):
            s = 0.0
            for k in range(m):
                s += a[i][k] * b[k][j]
            row.append(s)
        result.append(row)
    return result

import time
size = 60
A = [[1.0] * size for _ in range(size)]
B = [[1.0] * size for _ in range(size)]

t = time.time()
dot_naive(A, B)
elapsed = time.time() - t

print(f"{size}x{size} multiplication took {elapsed:.4f} s")
print("This is O(n^3): double the side and the time grows eightfold")
print(f"extrapolating, 600x600 would take about {elapsed * 1000:.1f} s")

The code is entirely correct. But if your project handles 1000×1000 matrices this implementation is unusable — and the AI will not volunteer that.

It does not know how large your n is. Only you do.

import time

# one problem, two data structures
def count_dups_list(items: list[int]) -> int:
    seen: list[int] = []
    count = 0
    for x in items:
        if x in seen:        # O(n) lookup
            count += 1
        else:
            seen.append(x)
    return count

def count_dups_set(items: list[int]) -> int:
    seen: set[int] = set()
    count = 0
    for x in items:
        if x in seen:        # O(1) lookup
            count += 1
        else:
            seen.add(x)
    return count

data = list(range(8000)) * 2
t = time.time(); count_dups_list(data); a = time.time() - t
t = time.time(); count_dups_set(data);  b = time.time() - t
print(f"with a list: {a:.4f} s")
print(f"with a set:  {b:.5f} s")
print(f"a factor of {a / b:.0f}. Both produce identical answers.")

Two: consistency across modules.

You asked ten questions and got ten locally sensible answers. Together they may have:

  • inconsistent naming (get_shape vs shape vs getShape)
  • inconsistent error handling (some return None, some raise)
  • the same helper implemented three times
  • different assumptions about the internal representation

An AI only sees the context you gave it. Global consistency is yours.

Three: your project’s own conventions.

“Shapes are always tuples here”, “every error subclasses ArrayError”, “indices are 0-based internally but 1-based in the public API” — it cannot know any of that unless you say so every time.

Four: judging whether the requirement itself is right.

If your requirement is flawed, an AI will faithfully implement something flawed. It will not ask whether you are sure.


3. Using AI for quality work

Getting tests written

Something an AI is genuinely good at — with one iron rule.

Write pytest tests for this function:

def reshape(self, *shape: int) -> "Array":
    """..."""

Cover: the normal case, mismatched element count, 1-D to 2-D,
2-D to 1-D, empty arrays, single-element arrays.

It will produce a batch of cases quickly. However:

Getting code reviewed

Hand it L8’s PEP 8 checklist:

Review this code against PEP 8, focusing on:
- snake_case / PascalCase naming
- shadowed built-ins
- overly long functions, overly deep nesting
- completeness of type hints
- mutable default arguments

Report problems only. Do not rewrite.

“Report problems only. Do not rewrite.” matters. Otherwise it returns a corrected version and you lose the chance to weigh each point.

And do not forget tooling: ruff is faster, deterministic, free, and does not hallucinate. If a tool can check it, do not ask an AI.

Getting documentation written

Write the "Usage" section of the README for this class, with:
- three examples, from simple to elaborate
- the output of each
- a note on the internal representation

Documentation plays to its strengths: the code is in front of it, and turning code into prose is one of the things it does best.

But verify the example outputs — it frequently invents them rather than computing them.


4. AI in a team

You are working in pairs. With an AI involved, that is really three parties.

Agree first, code second

Before the first line, the two of you should settle:

ItemWhy up front
internal representationchanging it later means starting over
naming stylerenaming afterward is painful
error-handling conventionraise or return None — it must be one or the other
formatting toolsblack plus ruff, configured in the project
test organizationwhere files live, how to run them

Put all of it at the top of the README. Paste that section into every prompt and half your consistency problems disappear.

Who is responsible for AI-written code

Whoever submits it. Not whoever wrote the prompt, and not the AI.

Which means: if you merge AI-produced code, you must be able to explain it. Asked about that section at the demo, “the AI wrote that” is not an answer.

Dependencies and licenses

An AI will sometimes suggest pulling in a third-party library. Before you do, ask:

  1. Do we actually need it? Will the standard library do? The team project also forbids implementing with NumPy
  2. What is its license? Some impose conditions on how you use it
  3. Is it maintained? A library last updated three years ago is a risk

5. When you must write it yourself

Not for integrity reasons but for effectiveness — in these situations an AI cannot help, or helping makes things slower:

One: things you do not yet understand.

If you do not know what matrix multiplication does, you cannot review the dot an AI gives you. Understand it first, then delegate. Otherwise you are accumulating code you cannot maintain.

Two: decisions between alternatives.

“Nested lists or a flat list with strides” depends on what you care about. An AI can lay out the pros and cons — genuinely useful — but the choice is your judgment.

Three: debugging.

An AI cannot see your runtime state. Given code and an error it guesses; you have a debugger (L8) and can see every variable’s actual value.

Locate the problem with a debugger; understand it with an AI. Not the other way round.

Four: the last mile of correctness.

When code is 90% right, the remaining 10% is usually an edge specific to your situation. An AI cannot help there, because it does not know your situation.


6. After this course

Tools will keep changing. Half the specific syntax you learned this term may be dated in five years.

Some things will not be:

Decomposing a problem. Turning a vague requirement into a set of clear small tasks — L7’s divide and conquer as a daily habit, and the precondition for using any tool at all.

Judging correctness. Knowing what to test, where the edges are, what will break. The stronger the AI, the more valuable this becomes — the more it can produce, the more there is to judge.

Estimating cost. Seeing a nested loop and thinking n²; seeing in and asking list or set. That is L11, and it is language-independent.

Explaining something clearly. Documentation, prompts, or talking to your partner — someone who can make a thing clear works faster with any tool.


Summary

  • At project scale the question is not “is this code right” but “is this design right”
  • Decompose yourself, down to tasks you can read in one sitting — decomposition is your job
  • Interface first: signature, types, error conditions, examples; then let the AI fill it in
  • Attach the relevant code, your conventions and existing tests to every prompt
  • One small step, verified each time. Asking for a pile and calling it working is the dangerous pattern
  • Good at: boilerplate, conversion, tests, docs, explaining errors, refactoring
  • Bad at: performance and complexity, cross-module consistency, your conventions, questioning the requirement
  • State the data scale in your prompt and results improve noticeably
  • AI-written tests cannot verify AI-written code — you list the edge cases first
  • Ask for review with “report problems only, do not rewrite”; use ruff where a tool suffices
  • Teams agree on representation, naming and error handling before coding
  • Whoever submits it owns it. “The AI wrote that” is not an answer
  • Write it yourself when: you do not understand it, it needs a trade-off, you are debugging, or it is the last mile

Exercises

  1. Take an unimplemented method from your team project and write a full interface specification (signature, docstring, three examples including edges) without any implementation. Hand it to an AI and assess what comes back.
  2. Have an AI implement dot(), then time it at 100×100 and 200×200. Verify it is O(n³). If it is, ask “how would you optimize this” and assess the suggestion.
  3. List at least eight edge cases for reshape() yourself, then have an AI generate tests and compare — which did it miss?
  4. Give an AI a module you wrote and one your partner wrote, and ask it only to report inconsistencies (naming, error handling, style). How many of its findings are real?
  5. Pick a piece of your partner’s code and ask them one question: “why is this line written this way?” Record the conversation — if they cannot answer, what have you discovered?
  6. Write your team’s AI usage agreement, half a page at most, covering at least: what may be delegated, what must happen before submitting, and who is responsible.

The lab problems are in Lab 15. The AI policy is now Level 2.