Worked review 1: CSV report CLI
This is the kind of script an AI assistant produces in one shot for “parse this CSV and give me a discount report”: it runs, it prints a plausible-looking report, and it has four real bugs. Read it once yourself before expanding any <details> below — find as many as you can, then compare.
import csvimport iofrom datetime import datetime
def parse_orders(csv_text: str, errors: list[str] = []) -> list[dict]: orders = [] reader = csv.DictReader(io.StringIO(csv_text)) for row in reader: try: row["amount"] = float(row["amount"]) orders.append(row) except: errors.append(row["order_id"]) return orders
def apply_discount(orders: list[dict], pct: float) -> list[dict]: for order in orders: order["amount"] = order["amount"] * (1 - pct) return orders
def build_report(orders: list[dict]) -> dict: total = sum(order["amount"] for order in orders) return { "generated_at": datetime.utcnow().isoformat(), "order_count": len(orders), "total": round(total, 2), }
BATCH_1 = """order_id,customer,amount1001,Ada,120.501002,Grace,not-a-number1003,Linus,89.99"""
BATCH_2 = """order_id,customer,amount2001,Bob,45.002002,Frank,not-a-number"""
orders_1 = parse_orders(BATCH_1)before_discount = orders_1[0]["amount"]discounted_1 = apply_discount(orders_1, 0.10)print("batch 1 order 1001 before discount:", before_discount)print("batch 1 order 1001 after discount:", orders_1[0]["amount"])
report_1 = build_report(discounted_1)print("report 1:", report_1)
orders_2 = parse_orders(BATCH_2)report_2 = build_report(orders_2)print("report 2:", report_2)
leaked_errors = parse_orders.__defaults__[0]print("shared default errors list after both batches:", leaked_errors)Loading Python runtime (first run only)…
Real output (the timestamps will differ on your run; everything else is exact):
batch 1 order 1001 before discount: 120.5batch 1 order 1001 after discount: 108.45report 1: {'generated_at': '2026-09-22T14:32:20.889615', 'order_count': 2, 'total': 189.44}report 2: {'generated_at': '2026-09-22T14:32:20.889744', 'order_count': 1, 'total': 45.0}shared default errors list after both batches: ['1002', '2002']Findings
Section titled “Findings”Bug 1: errors: list[str] = [] is a mutable default argument (catalog #1)
Symptom: parse_orders.__defaults__[0] — the actual default-argument object every call without its own errors list shares — contains ['1002', '2002'], from both batches, even though the two calls in the code above never pass an errors argument at all and look completely independent.
Evidence: the default list [] is created exactly once, when parse_orders is defined, not once per call. Every call that omits errors appends into that same shared object.
Fix: default to None and create a fresh list inside the function body:
def parse_orders(csv_text: str, errors: list[str] | None = None) -> list[dict]: if errors is None: errors = [] ...Bug 2: apply_discount mutates the caller’s list in place (catalog #3)
Symptom: before_discount (120.5) and orders_1[0]["amount"] after the call (108.45) are the same dict entry — apply_discount doesn’t return a new, discounted copy; it silently rewrites the amount field on every dict the caller passed in, and returns that same list.
Evidence: discounted_1 is orders_1 would be True — the function has no line that copies anything.
Fix: build new Order values instead of mutating in place, so the caller’s original data survives:
def apply_discount(orders: list[Order], pct: float) -> list[Order]: return [replace(order, amount=order.amount * (1 - pct)) for order in orders]Bug 3: bare except: swallows more than the intended parse failure (catalog #6)
Symptom: row 1002 (“not-a-number”) is silently recorded as an error and excluded from the report — which looks correct for this input. But the except: clause catches everything, not just the ValueError from float(...) failing: a missing "amount" column, a row that’s None, or any other genuine bug in this function would be swallowed exactly the same way, recorded as if it were just another bad row.
Evidence: running ruff on this file flags the bare except: directly (see What the tools caught below).
Fix: name the specific exception:
try: amount = float(row["amount"])except ValueError: errors.append(row["order_id"]) continueBug 4: datetime.utcnow() produces a naive, deprecated timestamp (catalog #19)
Symptom: running with -X dev prints a real DeprecationWarning pointing at this exact line, and the generated_at field in both reports carries no timezone information at all — a consumer of this report cannot tell whether "2026-09-22T14:32:20.889615" means UTC, the server’s local time, or something else.
Evidence — real warning:
DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).Fix:
"generated_at": datetime.now(timezone.utc).isoformat(),Fixed version
Section titled “Fixed version”All four bugs fixed, plus the underlying data model switched from loose dict rows to a small Order dataclass — the fix for bug 2 (returning new values instead of mutating) is far more natural once each order is a real, independently-constructed object rather than a shared dict:
import csvimport iofrom dataclasses import dataclass, replacefrom datetime import datetime, timezone
@dataclassclass Order: order_id: str customer: str amount: float
def parse_orders(csv_text: str, errors: list[str] | None = None) -> list[Order]: if errors is None: errors = [] orders: list[Order] = [] reader = csv.DictReader(io.StringIO(csv_text)) for row in reader: try: amount = float(row["amount"]) except ValueError: errors.append(row["order_id"]) continue orders.append(Order(order_id=row["order_id"], customer=row["customer"], amount=amount)) return orders
def apply_discount(orders: list[Order], pct: float) -> list[Order]: return [replace(order, amount=order.amount * (1 - pct)) for order in orders]
def build_report(orders: list[Order]) -> dict[str, object]: total = sum(order.amount for order in orders) return { "generated_at": datetime.now(timezone.utc).isoformat(), "order_count": len(orders), "total": round(total, 2), }
BATCH_1 = """order_id,customer,amount1001,Ada,120.501002,Grace,not-a-number1003,Linus,89.99"""
BATCH_2 = """order_id,customer,amount2001,Bob,45.002002,Frank,not-a-number"""
orders_1 = parse_orders(BATCH_1)before_discount = orders_1[0].amountdiscounted_1 = apply_discount(orders_1, 0.10)print("batch 1 order 1001 before discount:", before_discount)print("batch 1 order 1001 after discount (original unchanged):", orders_1[0].amount)
report_1 = build_report(discounted_1)print("report 1:", report_1)
orders_2 = parse_orders(BATCH_2)report_2 = build_report(orders_2)print("report 2:", report_2)
errors_1: list[str] = []parse_orders(BATCH_1, errors_1)errors_2: list[str] = []parse_orders(BATCH_2, errors_2)print("batch 1 errors:", errors_1)print("batch 2 errors (independent from batch 1):", errors_2)Loading Python runtime (first run only)…
Real output (timestamps will differ):
batch 1 order 1001 before discount: 120.5batch 1 order 1001 after discount (original unchanged): 120.5report 1: {'generated_at': '2026-09-22T14:33:23.884852+00:00', 'order_count': 2, 'total': 189.44}report 2: {'generated_at': '2026-09-22T14:33:23.885013+00:00', 'order_count': 1, 'total': 45.0}batch 1 errors: ['1002']batch 2 errors (independent from batch 1): ['2002']orders_1[0].amount is now unchanged after apply_discount (120.5, not 108.45), each batch’s errors stay independent, and generated_at now carries +00:00.
What the tools caught
Section titled “What the tools caught”ruff on the buggy file — three of the four bugs, directly:
B006 Do not use mutable data structures for argument defaultsE722 Do not use bare `except`DTZ003 `datetime.datetime.utcnow()` usedBug 2 (aliasing) is caught by nothing — no linter counts whether apply_discount’s docstring or contract promised not to mutate its argument.
ruff on the fixed file:
All checks passed!mypy --strict and pyright on the fixed file — both clean, once the loose dict rows became a typed Order dataclass:
mypy --strict: Success: no issues found in 1 source filepyright: 0 errors, 0 warnings, 0 informations