Worked review 1: CSV report CLI
นี่คือ script แบบที่ AI assistant ผลิตออกมาในครั้งเดียวสำหรับ “parse CSV นี้แล้วให้ discount report หน่อย”: มันรันได้ พิมพ์ report ที่ดูสมเหตุสมผล และมี bug จริง 4 ตัว อ่านมันเองรอบหนึ่งก่อนขยาย <details> ไหนก็ตามด้านล่าง หาให้ได้มากที่สุดเท่าที่จะทำได้ แล้วค่อยเทียบ
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)…
output จริง (timestamp จะต่างกันตอนคุณรัน ส่วนที่เหลือตรงเป๊ะ):
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']สิ่งที่พบ
หัวข้อที่มีชื่อว่า “สิ่งที่พบ”Bug 1: errors: list[str] = [] เป็น mutable default argument (catalog #1)
อาการ: parse_orders.__defaults__[0] — object ของ default argument จริงที่ทุก call ที่ไม่มี errors ของตัวเองแชร์กัน — มี ['1002', '2002'] จากทั้งสองแบตช์ ทั้งที่ทั้งสอง call ในโค้ดข้างบนไม่เคยส่ง argument errors เข้าไปเลย และดูเหมือนเป็นอิสระจากกันโดยสมบูรณ์
หลักฐาน: default value [] ถูกสร้างครั้งเดียวเป๊ะ ตอน parse_orders ถูก define ไม่ใช่ครั้งละ call ทุก call ที่พึ่ง default นี้แชร์ object เดียวกันนั้น
ทางแก้: default เป็น None แล้วสร้าง list ใหม่ข้างใน function body:
def parse_orders(csv_text: str, errors: list[str] | None = None) -> list[dict]: if errors is None: errors = [] ...Bug 2: apply_discount mutate list ของผู้เรียกในที่เดิม (catalog #3)
อาการ: before_discount (120.5) และ orders_1[0]["amount"] หลัง call (108.45) เป็น dict entry เดียวกัน — apply_discount ไม่ได้คืน copy ที่ลด discount แล้วตัวใหม่ มันเขียนทับ field amount บนทุก dict ที่ผู้เรียกส่งเข้ามาแบบเงียบ ๆ แล้วคืน list เดิมนั้นกลับไป
หลักฐาน: discounted_1 is orders_1 จะเป็น True — function ไม่มีบรรทัดไหน copy อะไรเลย
ทางแก้: สร้างค่า Order ใหม่แทนที่จะ mutate ในที่เดิม เพื่อให้ข้อมูลต้นฉบับของผู้เรียกรอด:
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: กลืนมากกว่าแค่ parse failure ที่ตั้งใจ (catalog #6)
อาการ: แถว 1002 (“not-a-number”) ถูกบันทึกเป็น error และถูกตัดออกจาก report อย่างเงียบ ๆ — ซึ่งดูถูกต้องสำหรับ input นี้ แต่ except: clause จับ ทุกอย่าง ไม่ใช่แค่ ValueError จาก float(...) ที่ fail: column "amount" ที่หายไป, row ที่เป็น None หรือ bug จริงอื่นใดใน function นี้จะถูกกลืนแบบเดียวกันเป๊ะ ถูกบันทึกราวกับเป็นแค่แถวที่ไม่ดีอีกแถวหนึ่ง
หลักฐาน: การรัน ruff บนไฟล์นี้ flag bare except: ตรง ๆ (ดู สิ่งที่เครื่องมือจับได้ ด้านล่าง)
ทางแก้: ระบุ exception เฉพาะเจาะจง:
try: amount = float(row["amount"])except ValueError: errors.append(row["order_id"]) continueBug 4: datetime.utcnow() ให้ timestamp แบบ naive ที่ deprecated แล้ว (catalog #19)
อาการ: การรันด้วย -X dev พิมพ์ DeprecationWarning จริงชี้ไปที่บรรทัดนี้เป๊ะ และ field generated_at ในทั้งสอง report ไม่มีข้อมูล timezone เลย — ผู้ใช้ report นี้บอกไม่ได้ว่า "2026-09-22T14:32:20.889615" หมายถึง UTC, เวลาท้องถิ่นของ server หรืออะไรอื่น
หลักฐาน — 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).ทางแก้:
"generated_at": datetime.now(timezone.utc).isoformat(),เวอร์ชันที่แก้แล้ว
หัวข้อที่มีชื่อว่า “เวอร์ชันที่แก้แล้ว”แก้ทั้งสี่ bug แล้ว บวกกับเปลี่ยน data model จาก dict row หลวม ๆ เป็น Order dataclass เล็ก ๆ — ทางแก้ของ bug 2 (คืนค่าใหม่แทนที่จะ mutate) เป็นธรรมชาติกว่ามากเมื่อแต่ละ order เป็น object จริงที่สร้างแยกจากกัน ไม่ใช่ 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)…
output จริง (timestamp จะต่างกัน):
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 ตอนนี้ไม่เปลี่ยนหลัง apply_discount (120.5 ไม่ใช่ 108.45), errors ของแต่ละแบตช์แยกจากกันจริง และ generated_at มี +00:00 แล้ว
สิ่งที่เครื่องมือจับได้
หัวข้อที่มีชื่อว่า “สิ่งที่เครื่องมือจับได้”ruff บนไฟล์ที่มี bug — สามในสี่ bug จับได้ตรง ๆ:
B006 Do not use mutable data structures for argument defaultsE722 Do not use bare `except`DTZ003 `datetime.datetime.utcnow()` usedBug 2 (aliasing) ไม่มีเครื่องมือไหนจับได้เลย — ไม่มี linter ตัวไหนนับว่า docstring หรือ contract ของ apply_discount สัญญาไว้ว่าจะไม่ mutate argument ของมัน
ruff บนไฟล์ที่แก้แล้ว:
All checks passed!mypy --strict และ pyright บนไฟล์ที่แก้แล้ว — สะอาดทั้งคู่ หลังจาก dict row หลวม ๆ กลายเป็น Order dataclass ที่ type แล้ว:
mypy --strict: Success: no issues found in 1 source filepyright: 0 errors, 0 warnings, 0 informations