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 question | This lecture’s question |
|---|---|
| is this code correct | is this design correct |
| does this function handle its edges | are these module interfaces sensible |
| how do I verify a return value | how do I verify a system |
| who writes this line, me or the AI | who 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:
- ask for one function
- read it — if you do not follow it, ask for an explanation or a different approach
- test it — with cases you prepared
- only then ask for the next one
2. What an AI does well and badly
Well
| Task | Why it suits |
|---|---|
| boilerplate | mechanical, repetitive, patterned |
| format conversion | input and output both explicit |
| writing test cases | it enumerates common situations well (edges are on you) |
| writing docstrings | the code is right there; describing it is its strength |
| explaining errors | it has seen enormous numbers of them |
| refactoring | clear instructions like “rewrite this as a comprehension” |
| looking up API usage | faster 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_shapevsshapevsgetShape) - 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:
| Item | Why up front |
|---|---|
| internal representation | changing it later means starting over |
| naming style | renaming afterward is painful |
| error-handling convention | raise or return None — it must be one or the other |
| formatting tools | black plus ruff, configured in the project |
| test organization | where 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:
- Do we actually need it? Will the standard library do? The team project also forbids implementing with NumPy
- What is its license? Some impose conditions on how you use it
- 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
ruffwhere 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
- 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.
- 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. - List at least eight edge cases for
reshape()yourself, then have an AI generate tests and compare — which did it miss? - 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?
- 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?
- 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.