Three questions. Same box you already type in, no new tool, no extra tab, nothing to install:
- Why is this line here?
- What breaks if I change this line?
- What would you have written instead, and why is it different?
The questions are not the clever part. The timing is. You ask them while the code is still arriving — in the same thread that produced it, before you have pasted it anywhere and moved on.
These three are mine. What is not mine is the finding underneath them, from a trial I will get to at the end: asking an assistant things is not one behaviour.
Some kinds of asking preserve your understanding, some spend it, and the difference showed up on a test.
These are my attempt to turn that into something you can type on a Monday. Four minutes.
The window closes faster than you think
In the thirty seconds after an assistant hands you nine lines of code, three things are true.
The assistant still has the whole conversation in front of it. You still have the question, because you have not yet been distracted by the code working.
And you have built nothing on top of it, so a different version is free.
All three expire. Later that week the thread is gone, the question has faded into "it works, leave it," and four other files assume that function behaves exactly the way it behaves.
Asking why a pipe runs where it runs is free while the wall is open. Once the plasterboard is up, the same question costs you a morning and a crowbar.
The failure mode that prevents: a project you can extend but cannot repair.
The code we will ask about
Ask any assistant for a function that removes duplicates from a list but keeps the original order, and you get roughly this:
def dedupe(items):
seen = set()
out = []
for item in items:
if item not in seen:
seen.add(item)
out.append(item)
return out
print(dedupe(["red", "blue", "red", "green", "blue"]))
Save it as dedupe.py, run 'python3 dedupe.py', and you get:
['red', 'blue', 'green']
Correct. Nothing in this file is a bug, and I picked it for that reason — all three questions pay out on code that is already right. Everything below was run on Python 3.11.15.
Question one: why is this line here?
Pick the line you would not have written yourself. Mine is the first one: 'seen = set()'.
There are two collections in a nine-line function doing what looks like one job. 'out' already holds every item we have kept. So paste the line back and ask: why is this line here, and what is it doing that 'out' is not?
The short answer is that 'item not in out' searches a list one element at a time, top to bottom, every single loop. 'item not in seen' looks the item up by its hash — one step, and the step does not get slower as the collection grows.
Notice the shape of a useful answer. Not "sets are faster," a slogan you cannot act on, but "here is the specific operation that got cheaper," which you can go and measure.
Question two: what breaks if I change this line?
So change it. In dedupe.py, rename the original to 'dedupe_set', delete the 'print' line so the file can be imported quietly, and add a version that searches the list instead — one line different:
def dedupe_list(items):
out = []
for item in items:
if item not in out: # the only change: search the list, not the set
out.append(item)
return out
On those five colours it returns the same thing. Time both, 200,000 calls each, from the directory holding dedupe.py:
python3 -m timeit -n 200000 -r 3 \ -s "from dedupe import dedupe_set; c=['red','blue','red','green','blue']" \ "dedupe_set(c)" python3 -m timeit -n 200000 -r 3 \ -s "from dedupe import dedupe_list; c=['red','blue','red','green','blue']" \ "dedupe_list(c)"
200000 loops, best of 3: 290 nsec per loop 200000 loops, best of 3: 208 nsec per loop
The version without the set wins, because building a set costs something and on five items you never earn it back.
That is where most of us would stop, and it is wrong in a way you cannot see at that size. Give it a real list — save this next to dedupe.py as bench.py:
import random, time from dedupe import dedupe_set, dedupe_list random.seed(0) data = [random.randint(0, 50000) for _ in range(50000)] for label, fn in (("set ", dedupe_set), ("list ", dedupe_list)): start = time.perf_counter() result = fn(data) print(f"{label} {len(result)} unique in {time.perf_counter() - start:.3f}s")
set 31585 unique in 0.006s list 31585 unique in 3.652s
Same 31,585 items out. One finished before you could register it; the other took nearly four seconds — three runs here gave 582, 665 and 754 times the wait.
Your own seconds will differ, and that is fine: several hundred times is the finding, not the number.
So that is what 'seen = set()' holds up. Not correctness: the version without it is correct. It holds up the difference between a page that responds and a page that sits for four seconds while somebody decides your app is broken.
Ask in the box first, then run it. Asking gets you the prediction. Running is why you no longer have to decide whether to believe it.
Question three: what would you have written instead, and why is it different?
This is the one people skip, and it hands you things you could not have searched for, because you cannot search for a thing you have not heard of yet.
Ask it, and for this function you get a single line:
print(list(dict.fromkeys(["red", "blue", "red", "green", "blue"])))
['red', 'blue', 'green']
Dictionary keys are unique, and since Python 3.7 they remember the order they were added in, so 'dict.fromkeys' does both jobs the loop was doing at once. On the same 50,000 numbers it returns output identical to 'dedupe_set' and it is faster — but only just. Add a third row to the tuple in bench.py and rerun 'python3 bench.py':
for label, fn in (("set ", dedupe_set), ("list ", dedupe_list), ("keys ", lambda d: list(dict.fromkeys(d)))):
Five runs here: 1.14x, 1.11x, 1.64x, 1.19x, 1.48x. If you measure 1.2x you did not run it wrong. That is a small constant-factor win inside the noise — a different kind of claim from the several-hundred-times gap above, which was an algorithm changing shape.
Then spend one more question — "when would the loop be better than this?" — and you get the other half of it. 'dedupe_set' and 'dict.fromkeys' both need items that can be hashed. 'dedupe_list', still on the page from question two, does not, and that difference is louder than it looks. Give all three the same list of lists, '[[1], [2], [1]]':
dedupe_set TypeError: unhashable type: 'list' dict.fromkeys TypeError: unhashable type: 'list' dedupe_list [[1], [2]]
Two stop. The third quietly succeeds on the same input. And the loop is the one you can put a condition inside: lowercase before comparing, strip the whitespace, treat two spellings as the same customer.
That is an honest place to stand: a one-liner you understand, and the shape of the day you will want the loop back.
Where the three questions came from
"How AI Impacts Skill Formation", by Judy Hanwen Shen and Alex Tamkin (arXiv:2601.20245, 3 February 2026), is a randomised trial in which developers learned an unfamiliar asynchronous Python library — Trio — from a self-guided tutorial, after a warm-up coding task.
The treatment group could ask an AI conceptual questions or have it generate code. Note that setting, because it bounds the claim: this measured skill formation on unfamiliar material, narrower than any nine lines an assistant hands you.
The assisted group scored 17% lower on comprehension afterwards. The gap was widest on the debugging questions — the ones that ask where you would look if this went wrong — and narrowest on plain code reading.
On speed, in plain words, because the technical phrasing gets read as the opposite of what it means: they finished a little faster, but the speed difference was small enough that it could be chance, and the comprehension gap was not.
The part worth your Monday is inside the assisted group. The authors identified six distinct interaction patterns, three of which preserved learning outcomes, and they name two of them directly: high scorers "only asked AI conceptual questions instead of code generation," or "asked for explanations to accompany generated code."
Read that first one again, because it cuts against the pitch I am making. Some of the people who kept their comprehension were not asking better questions about generated code. They were not asking it for code at all.
So, the honest accounting. Questions one and two are conceptual questions about code you already have — that is the second pattern, explanations accompanying generated code.
Question three asks for code, which is the behaviour on the losing side of that split.
Which is why "and why is it different" is not decoration: it is the clause that drags the request back across the line, and I would rather you skipped the question than asked its first half alone.
The trial does not test that. That part is mine.
Why this matters
Sure, but — doesn't stopping to ask three questions eat the speed you came for?
Count what it cost: three questions and two runs of a nine-line function. Four minutes, and I am being generous with the arithmetic. What you bought is three lines you will never guess about again, in a language you are going to keep using.
And the version where you skip it is not faster. It is later. Comprehension does not get skipped, it gets deferred — to the evening the thing breaks, when the thread is gone, the assistant has no idea what you are talking about, and the only person available to explain that code is you.
This is not the same job as reading the file afterwards, which is worth doing and which we walked through in The Bug That Doesn’t Crash Anything.
Reading a finished file is archaeology. This is asking the builder a question while he is still in your kitchen.
Final thought
The next thing you ask for — a function, a route, a bit of CSS — do not accept it straight away.
Find the one line in it you would not have written yourself, paste that line back, and type five words: why is this line here?
One line, one question, before you move on. Then ask what breaks if you change it, and go break it.