Since late 2025, some companies hand you an AI assistant in the interview and grade how you drive it. That is how mainstream AI assisted coding has become: the typing is cheap now, and the judgment is the job. This post shows the loop that keeps you safe, spec first, small diffs, verify everything, by extending a real project and catching a planted bug.
“Debugging is twice as hard as writing the code in the first place. So if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”
Brian Kernighan
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 18 minutes
Kernighan wrote that line decades before any AI wrote a function for you, and it is the whole reason this topic matters. An assistant writes clever code faster than you ever could, which means the hard part, understanding and debugging it, lands entirely on you. Think of the assistant as a very fast, very confident intern. It types quicker than anyone on the team, it never gets tired, and it will state a wrong answer with exactly the same calm certainty as a right one. You would never let an intern push straight to the main branch without a review, and the same rule keeps you safe here.
AI assisted coding is not a fringe skill any more. A recent Dataiku job posting for a senior Python engineer lists “proficiency with AI assisted development tools” as a plain requirement next to testing and Git, and since around October 2025 several companies, Meta among them, have added an AI enabled coding round where you are handed an assistant and graded on how well you drive and verify it. Almost nobody teaches the actual workflow, so let us fix that.
Table of Contents
The Skill Nobody Teaches: Steering, Not Typing
Most people use an assistant like a slot machine: type a vague wish, pull the handle, paste whatever falls out, and hope. That is how bad code sneaks into production. The professional habit is the opposite, and it rests on three moves you already half know from ordinary engineering.
- Spec first. Before you prompt, write one or two sentences of exactly what “done” means, the same acceptance list you would write for yourself. A vague prompt gets a vague, confident guess back.
- Small diffs. Ask for one function or one change at a time. A 200 line generated blob is impossible to review honestly, so you end up trusting it, which is the whole trap.
- Verify before you accept. Nothing the assistant writes is real until a test passes and your linter and type checker stay quiet. You are the gate, not the keyboard.
This is the exact review loop from our code review tutorial, just pointed at a machine instead of a colleague. The reviewer does not care who wrote the line; the standard is the same. The diagram below is the loop you run for every single change, whether you typed it or an assistant did.
Read the loop from the top. You write the spec, prompt for one small task, and the assistant returns a diff you have not accepted yet. You read that diff: is it small, and does it do what the spec asked? If it looks wrong, you reject it or re prompt with the specific problem. If it looks right, it still has to pass the gate: your tests, plus ruff and mypy from our code quality tutorial. Only when everything is green do you commit, and the moment you commit, that code is yours. Not the assistant’s. Yours to defend in review and fix at 2am.
A Worked Example: Adding a Report with an Assistant
Let us make this real. In the log parser project we built a small Command-Line Interface (CLI) that reads an Apache access log and reports errors, top IPs, and error spikes. Say a developer named Aviraj wants to add one more report: the most requested URL paths. That is a perfect task to hand an assistant, because it follows a pattern already in the file.
Step one is the spec, written before any prompt. Here is exactly what Aviraj types to the assistant.
📄 the prompt: a spec, not a wish
Add a `paths` report to logparse.py that ranks the most requested URL paths, most frequent first. Match the existing cmd_top style: take entries and args, use args.top for how many rows, return a string, and register it in the COMMANDS dict. Standard library only.
The assistant replies in seconds with a tidy diff. It reads well, it follows the house style, and it is short enough to review. Here is what it proposed.
📄 the diff the assistant generated
+def cmd_paths(entries, args):
+ counts = Counter(e.path for e in entries)
+ top = counts.top(args.top)
+ lines = [f"Top {args.top} requested paths"]
+ for path, n in top:
+ lines.append(f" {path:<20} {n}")
+ return "\n".join(lines)
+
+
-COMMANDS = {"top": cmd_top}
+COMMANDS = {"top": cmd_top, "paths": cmd_paths}
Look closely, because this is where a slot machine user loses. The line counts.top(args.top) looks completely natural. It reads like real English, it sits inside otherwise correct code, and if you were skimming you would accept it. So do not skim. Run it.
📄 Terminal: run the report before trusting it
python logparse.py paths access.log --top 5
▶ Output
Traceback (most recent call last):
File "logparse.py", line 91, in <module>
main()
~~~~^^
File "logparse.py", line 86, in main
result = COMMANDS[args.command](entries, args)
File "logparse.py", line 63, in cmd_paths
top = counts.top(args.top)
^^^^^^^^^^
AttributeError: 'Counter' object has no attribute 'top'. Did you mean: 'pop'?
What happened here: the assistant invented a method. collections.Counter has no .top(); the real method for the top n items is .most_common(n), which you used two functions up in cmd_top. This is a hallucinated Application Programming Interface (API), the single most common way AI code fails, and Python caught it the instant the code ran. The interpreter even guessed you meant pop, which you did not. If Aviraj had pasted this and moved on, the crash would have shown up later, maybe in front of a user.
Running it once by hand is good, but the durable fix is a test, so this bug can never come back silently. Write the test before you accept the code, based on the spec, not on what the assistant wrote. Six requests, three paths, ask for the top two: /a should rank first and /c should not appear at all.
📄 test_paths.py: written from the spec, not the code
from types import SimpleNamespace
from datetime import datetime, timezone
from logparse import cmd_paths, LogEntry
def make(path):
return LogEntry("1.1.1.1", datetime.now(timezone.utc), "GET", path, 200, 100)
def test_paths_ranks_by_count():
entries = [make("/a"), make("/a"), make("/a"),
make("/b"), make("/b"), make("/c")]
args = SimpleNamespace(top=2, json=False)
out = cmd_paths(entries, args)
assert "/a" in out
assert "/c" not in out # only the top 2 should show
assert out.index("/a") < out.index("/b") # /a is most requested
📄 Terminal: run the test against the AI code
pytest test_paths.py -q
▶ Output
F [100%]
================================== FAILURES ===================================
__________________________ test_paths_ranks_by_count __________________________
def cmd_paths(entries, args):
counts = Counter(e.path for e in entries)
> top = counts.top(args.top)
^^^^^^^^^^
E AttributeError: 'Counter' object has no attribute 'top'. Did you mean: 'pop'?
logparse.py:63: AttributeError
=========================== short test summary info ===========================
FAILED test_paths.py::test_paths_ranks_by_count - AttributeError: 'Counter' o...
1 failed in 0.16s
What happened here: the test failed for the same reason the manual run did, which is exactly what you want. Now the fix is one word. Change .top to .most_common, the method that actually exists, and run the test again.
📄 the one line fix
- top = counts.top(args.top) + top = counts.most_common(args.top)
▶ Output: pytest after the fix
. [100%] 1 passed in 0.02s
▶ Output: the real report on the 10,000 line log
Top 5 requested paths / 2531 /api/orders 1933 /api/login 1579 /api/products 988 /static/app.css 785
What happened here: the test passes, the report runs on the real log, and only now does Aviraj commit. The assistant did the boring typing and got the shape 90 percent right, which genuinely saved time. The spec told it what to build, the manual run and the test caught the one thing it got wrong, and the fix took five seconds because the failure pointed straight at the line. That is the loop. Notice that the human never had to be smarter than the assistant, only more careful.
The Failure Taxonomy: How AI Code Goes Wrong
Once you have watched a few of these bugs, you start to recognise families. AI code fails in a small number of repeatable ways, and each one has a cheap catch. Learn the table below and you will spot most problems before they cost you anything.
| Failure | What it looks like | How you catch it |
|---|---|---|
| Hallucinated API | A method or import that does not exist, like Counter.top() | Run it once; AttributeError or ImportError fires immediately |
| Subtle off-by-one | Slicing [:n-1] when it meant [:n]; returns one row short | A test that asserts the count, not just the type |
| Stale API knowledge | Old syntax or a wrong return type from a changed library | mypy flags the type mismatch before runtime |
| Overconfident refactor | A tidy rewrite that also quietly drops a case or an import | ruff for dead code, plus the existing test suite |
Two of these never reach a human if you have the quality gate from the code quality tutorial wired in. Suppose an assistant refactors that report and, being tidy, adds an import it does not use and returns the wrong type. Here is the kind of snippet it hands back.
📄 ai_snippet.py: an overconfident little rewrite
import itertools
from collections import Counter, OrderedDict
def busiest_path(entries: list) -> str:
counts = Counter(e.path for e in entries)
ranked = OrderedDict(counts.most_common())
return ranked.get("/api/login")
📄 Terminal: let ruff and mypy read it
ruff check ai_snippet.py mypy ai_snippet.py
▶ Output
F401 [*] `itertools` imported but unused --> ai_snippet.py:1:8 | 1 | import itertools | ^^^^^^^^^ help: Remove unused import: `itertools` Found 1 error. [*] 1 fixable with the `--fix` option. ai_snippet.py:8: error: Incompatible return value type (got "Any | None", expected "str") [return-value] Found 1 error in 1 file (checked 1 source file)
What happened here: two tools, two bugs caught, no cleverness required. Ruff spotted the phantom itertools import the assistant sprinkled in for no reason. Mypy caught the real trap: OrderedDict.get returns str | None because the key might be missing, but the function promised a plain str. That is a stale API assumption dressed up as clean code, and it would have handed None to whatever called it. Neither bug needed you to be clever; you just had the gate on. This is why the workflow leans so hard on the same tools you already run for your own code.
The off-by-one deserves one more look, because it is the quietest of the family. It does not crash and it passes a type check, so only a test that checks the actual count will ever see it.
📄 offbyone.py: a rewrite that returns one row short
from collections import Counter
paths = ["/a"] * 5 + ["/b"] * 4 + ["/c"] * 3 + ["/d"] * 2 + ["/e"]
counts = Counter(paths)
def top_paths(n: int) -> list[tuple[str, int]]:
ranked = counts.most_common()
return ranked[:n - 1] # the assistant's off-by-one: should be [:n]
rows = top_paths(3)
print("asked for top 3, got", len(rows), "rows:", rows)
assert len(rows) == 3, f"expected 3 rows, got {len(rows)}"
▶ Output
asked for top 3, got 2 rows: [('/a', 5), ('/b', 4)]
Traceback (most recent call last):
File "offbyone.py", line 12, in <module>
assert len(rows) == 3, f"expected 3 rows, got {len(rows)}"
^^^^^^^^^^^^^^
AssertionError: expected 3 rows, got 2
What happened here: the code ran happily and returned a list of the right type, so nothing shouted. The n - 1 slice quietly gave back two rows when three were asked for, the sort of thing that looks fine in a demo and is wrong in production. Only the assert on the count exposed it. This is the lesson behind “test the behaviour, not the type”: an assistant is very good at producing code that has the right shape and the wrong answer.
Where AI Wins and Where It Burns Time
An assistant is a power tool, and like any power tool it is brilliant for some jobs and a menace for others. Knowing the difference is most of the skill. Here is the honest split from real day to day AI assisted coding.
- Wins: boilerplate. Argparse setups, dataclass definitions, a fresh pytest file, a regex for a known format. Repetitive code with a clear shape is where the assistant genuinely saves you minutes every time.
- Wins: first draft tests. “Write pytest cases for this function” gets you a solid starting set fast. You still read them and add the edge cases it missed, but the blank page problem is gone.
- Wins: explain this code. Point it at a gnarly function someone else wrote and ask what it does. It is a fast, patient explainer, and you can verify its answer by reading the code it just described.
- Burns time: fuzzy requirements. If you cannot write the spec, the assistant cannot either. It will produce confident code for the wrong problem, and you lose more time untangling it than you would have spent thinking first.
- Burns time: large cross file changes. Ask it to rewire five modules at once and you get a diff too big to review, so you either trust it blindly or re read the whole thing. Both are slower than small steps.
- Burns time: anything novel. A brand new algorithm or a fresh library released after its training cutoff is where hallucinations cluster. When you are on genuinely new ground, the machine is guessing, and you are back to reading the docs yourself.
Common Mistakes
Mistake 1: Accepting a diff you did not read
The one click “accept” button is where most AI bugs enter a codebase. If the change is too big to read, it is too big to accept: ask for a smaller one. The moment you commit, that code is yours to explain in review, so read every line as if you had typed it, because as far as the team is concerned, you did.
Mistake 2: Prompting a wish instead of a spec
“Make the log parser better” gets you a confident guess at what “better” means. “Add a paths report that ranks URLs by request count, top N, standard library only” gets you the thing you wanted. The narrower the spec, the smaller the diff, and the easier it is to verify. Vague in, vague out.
Mistake 3: Trusting confidence as correctness
An assistant states a hallucinated method with the exact same steady tone it uses for a correct one. There is no wobble in its voice when it is wrong. Confidence is not evidence. A passing test is evidence, a clean mypy run is evidence, the docs are evidence. Trust those, not the tone.
Best Practices
- DO write the spec before the prompt, even one sentence, so you know when the answer is right
- DO ask for one small change at a time, sized so you can honestly review the whole diff
- DO write the test from the spec, not from the generated code, so a hallucination cannot pass it
- DO run
ruffandmypyon generated code exactly as you do on your own - DON’T paste a change you have not read, no matter how tidy it looks
- DON’T reach for the assistant on a fuzzy or genuinely novel problem before you have thought it through yourself
The Interview Angle: AI-Enabled Coding Rounds
The newer AI enabled rounds are not testing whether you can type a loop from memory. They hand you an assistant and watch how you drive it. What earns the offer is exactly the loop in this post: you say your plan out loud, you prompt in small steps, and when the assistant hands you code, you read it critically and verify it before moving on. Catching a hallucinated method and fixing it with a test is a strong signal, far stronger than never hitting a bug because you never looked.
What fails the round is pasting generated code without reading it, or freezing when it does not work. Interviewers know the tools produce bugs; they are watching to see whether you notice. Narrate your verification. Say “I will run this before I trust it” and then actually run it. Treat the assistant as a fast junior whose work you own, and you will look exactly like the engineer they want to hire.
Conclusion
AI assisted coding does not replace the skills in this series, it raises the value of them. Reading code, writing tests, and running ruff and mypy matter more now, not less, because they are how you verify a machine that types faster than you and is wrong often enough to hurt. The workflow is simple and it lasts: spec first, small diffs, verify before you accept, and own everything you commit. Cursor, Claude Code, and GitHub Copilot are the names at the time of writing, and by the time you read this some of them may have changed, but that loop will not. The tool is disposable; the discipline is the career.
Next, practice AI assisted coding on one of your own projects: write the spec, ask for one small change, and refuse to accept it until a test passes. And if you want the full path from first steps through AI and machine learning, browse the Python + AI/ML tutorial series home.
Frequently Asked Questions
What is AI assisted coding?
AI assisted coding is writing software with a code generating assistant such as Cursor, Claude Code, or GitHub Copilot, where the tool drafts code from your prompt and you steer, review, and verify it. The skill is not typing prompts, it is writing a clear spec, keeping each change small, and testing every generated line before you accept it.
Does using an AI assistant mean I do not need to learn Python?
No, the opposite. The assistant produces confident code that is wrong often enough to matter, so you need to read code, write tests, and run type checks to catch its mistakes. The tool raises the value of core skills because you are now the reviewer of a very fast, sometimes wrong, junior developer.
What is the most common way AI generated code fails?
Hallucinated APIs: the assistant confidently calls a method or imports a module that does not exist, like Counter.top() instead of Counter.most_common(). These crash the instant the code runs, which is why running generated code once, before trusting it, catches most of them immediately.
How do I catch AI bugs that do not crash?
Use tests and the quality gate. A test that asserts on behaviour catches subtle off-by-ones that return the wrong count without crashing. Ruff catches phantom imports and dead code, and mypy catches wrong return types and stale API assumptions before they ever run.
Will these specific tools still exist in a few years?
The names will change. Cursor, Claude Code, and GitHub Copilot are the leaders at the time of writing in mid 2026, and the market moves fast. What does not change is the AI assisted coding workflow: write a spec, keep diffs small, and verify with tests and type checks. Learn the loop, not the logo.
Is it cheating to use an AI assistant in a coding interview?
Not in the newer AI enabled rounds, where you are handed an assistant on purpose and graded on how well you drive and verify it. In a traditional round without one, follow the instructions you were given. When a tool is allowed, using it well and checking its output is the skill being tested.
Interview Questions on AI-Assisted Coding
Scenario questions, not trivia: this is the form this topic takes in a real interview.
Q: An assistant hands you a 60 line function that looks correct. How do you decide whether to accept it?
First, I would not accept a 60 line diff at all if I could avoid it; I would ask for the change in smaller pieces I can actually review. Given the block, I read every line against the spec I wrote before prompting, run it once to catch any hallucinated call, then write or run tests that assert on the behaviour I expect, not just that it returns without error. Finally I run ruff and mypy. Only when all of that is green do I commit, because the moment I commit it is my code, not the assistant’s.
Q: The assistant used a method you have never seen. What do you do?
I do not assume it exists just because the code reads well; hallucinated methods are the most common AI failure. I check the official documentation for that method, or I simply run the code, since a made up method raises AttributeError immediately. If it is real but unfamiliar, reading the docs also tells me its return type and edge cases, which is exactly what I need before I depend on it.
Q: Why write the test from the spec rather than from the generated code?
If I write the test to match what the assistant produced, I am just encoding its assumptions, including any bug, so the test passes on wrong code. A test written from the spec describes the behaviour I actually want, independent of the implementation. That is what caught the off-by-one earlier: the code ran and returned the right type, but the test asserted the correct row count and failed, which is precisely the mistake I needed to see.
Q: Where does an AI assistant genuinely save time, and where does it cost you time?
It wins on boilerplate with a clear shape: argparse setups, dataclasses, a first draft of tests, and explaining unfamiliar code. It burns time on fuzzy requirements, because it will confidently build the wrong thing, and on large cross file refactors, where the diff gets too big to review honestly. It also struggles on genuinely novel work or libraries newer than its training, which is where hallucinations cluster. The rule of thumb is: clear and repetitive, let it draft; fuzzy or novel, think first.
Q: In an AI enabled coding round, what are the interviewers actually grading?
They are grading how you drive and verify the tool, not whether you can code from memory. They want to see a plan stated out loud, small prompts, critical reading of the output, and real verification: running the code, writing a test, checking the docs when something looks off. Catching and fixing a hallucination is a positive signal. Pasting generated code without reading it, or freezing when it breaks, is what fails the round.
Q: These tools change fast. How do you future proof the skill?
By learning the workflow rather than any one product. Spec first, small diffs, verify before you accept, and own what you commit; that loop works the same whether the assistant is Cursor, Claude Code, Copilot, or something that does not exist yet. The verification tools, tests, ruff, and mypy, are also stable and vendor neutral. When the tool names change, my process does not, which is exactly why I invest in the process.
Further reading: the official Python documentation is the authoritative source on this.
Related Posts
Previous: Python: CI/CD for Python with GitHub Actions, Pre-commit, Automated Testing
Next: Python: Security Basics, Secrets, Input Validation, Dependency Scanning
Series Home: Python + AI/ML Tutorial Series

No comment