I've been using AI coding tools heavily for a while now, and the thing that changed my workflow the most wasn't the code generation itself, it was building a review habit that actually catches the failure modes specific to generated code. AI output tends to look more finished than it is. The variable names are good, the structure is clean, the comments are confident. That polish is exactly what makes it easy to under-review.
This is the checklist I actually run through, not the one I aspire to run through. Eight categories, each with a quick explanation of why it matters and a small example of the kind of thing that slips past a fast skim.
1. Correctness: does it do what was asked, not just something plausible
AI models are very good at producing code that satisfies the letter of a prompt while missing the actual intent. If you ask for "a function that returns the most recent order for a user," you need to check what "most recent" means in the data. Sorted by creation date? Updated date? What if there are two orders with the same timestamp?
def get_most_recent_order(orders):
return sorted(orders, key=lambda o: o.created_at)[-1]
This works until two orders share a timestamp, and then the result is whichever one Python's sort happens to place last, which is not "most recent" in any meaningful sense. It's arbitrary. Nobody asked for arbitrary.
Why it matters: correctness bugs from AI output are rarely syntax errors, they're semantic gaps between what was asked and what got built. You have to read the code against the requirement, not just against itself.
2. Edge cases: the inputs nobody put in the prompt
If your prompt didn't mention empty lists, null values, negative numbers, or duplicate entries, the model probably didn't think about them either. It optimizes for the happy path described in the request.
function calculateAverage(scores) {
const total = scores.reduce((sum, s) => sum + s, 0);
return total / scores.length;
}
Pass an empty array and you get NaN silently propagating into whatever calls this next. No error, no crash, just a quietly wrong number working its way downstream.
Why it matters: edge cases are where AI-generated code fails most often, because the model is pattern-matching against the common case in its training data, not reasoning about your specific data guarantees.
3. Security: don't assume the model thought about it unless you told it to
If you didn't explicitly ask for input sanitization, parameterized queries, or auth checks, don't assume they're there. They often aren't, because the prompt didn't ask for them.
query = f"SELECT * FROM users WHERE email = '{user_email}'"
cursor.execute(query)
This is straightforward SQL injection, and it's also one of the most common patterns I see generated when someone asks for "a function that looks up a user by email" without mentioning security at all.
Why it matters: generated code defaults to the simplest version of a solution, and the simplest version is very often the insecure version. Treat every generated data access or input-handling function as unreviewed for security until you've explicitly checked it.
4. Performance: check the complexity, not just the output
Generated code frequently reaches for the most obvious algorithm rather than the most efficient one, especially for anything involving lookups or nested iteration.
def find_duplicates(items):
duplicates = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
if items[i] == items[j] and items[i] not in duplicates:
duplicates.append(items[i])
return duplicates
This is O(n²) for a problem that's a few lines away from O(n) with a set or a counter. It'll pass every test on a small list and quietly become a bottleneck the moment someone runs it against production-sized data.
Why it matters: small inputs during testing hide complexity problems that only show up at scale, and AI output doesn't come with a complexity analysis attached. You have to do that part yourself.
5. Maintainability: will the next person understand this without you
Generated code is often correct in isolation but disconnected from the conventions of the rest of your codebase. Different error handling style, different naming pattern, a slightly different way of structuring similar logic that already exists three files over.
# existing pattern in the codebase
class UserNotFoundError(Exception):
pass
# what got generated for a new function
def get_user(user_id):
user = db.query(user_id)
if not user:
return None # inconsistent with the rest of the codebase's error pattern
return user
Why it matters: consistency is a maintainability feature. Code that solves the problem correctly but ignores established patterns adds friction for every future reader, even if it never causes a bug.
6. Tests: check what's actually being asserted, not just that tests exist
Generated tests can create a false sense of safety. It's common to get tests that run without error but don't assert anything meaningful.
def test_process_payment():
result = process_payment(order)
assert result is not None
This test passes whether the payment succeeded, failed silently, or returned an empty error object. It confirms the function returned something, not that it did the right thing.
Why it matters: a test suite with weak assertions is worse than no test suite in some ways, because it creates confidence that isn't backed by actual verification. Read the assertions, not just the test count.
7. Dependencies: verify the package actually exists and does what's claimed
Models occasionally reference packages, methods, or APIs that don't exist, or that existed in an older version and have since changed. This is less common than it used to be but still worth a direct check, especially for less mainstream libraries.
import pandas as pd
df.iteritems() # removed in pandas 2.0, replaced by df.items()
Code like this can look completely reasonable to someone who hasn't kept up with a specific library's recent changes, and it'll fail immediately in CI or, worse, only in a slightly older pinned environment.
Why it matters: a broken import or a deprecated method is an easy fix once caught, but it's an annoying one to debug blind if it ships and fails somewhere downstream instead of at review time.
8. Production behavior: what happens when a dependency it relies on goes down
This is the category that's easiest to skip because it doesn't show up in normal testing at all. Does the code handle timeouts? Partial failures? What does logging look like when something goes wrong at 3am and someone's trying to debug it without you in the room?
def send_notification(user_id, message):
response = requests.post(NOTIFICATION_API_URL, json={"user_id": user_id, "message": message})
return response.json()
No timeout, no retry, no handling of a non-200 response, and no logging if it fails. When the notification service has a bad five minutes, this function will hang or throw an unhandled exception with zero context about what actually happened.
Why it matters: this is the category of bug that doesn't show up until it's already in production, because local development rarely simulates a flaky dependency. It's worth explicitly asking "what does this look like when the thing it depends on is unavailable" for anything that talks to an external service.
The pattern across all eight
Every one of these examples looks fine at a glance. That's not a coincidence, it's the actual challenge with reviewing generated code. The syntax is clean, the naming is sensible, the code reads as intentional. The gaps aren't in how the code looks, they're in what it doesn't account for, and you only catch that by asking specific questions of it rather than scanning for things that look wrong.
Save this, print it, stick it next to your monitor, whatever works. The categories don't change much project to project, only the specifics do.
What would you add to this list? Curious what's bitten people that isn't covered here.
Top comments (0)