Verification tools
output ทุกอันด้านล่างเป็นของจริง จับมาจากการรันแต่ละเครื่องมือกับ pattern จาก AI Bug Catalog ไม่มีเครื่องมือไหนครอบคลุมซ้อนกันสมบูรณ์ และไม่มีตัวไหนใกล้เคียงกับความสมบูรณ์เลย — ประเด็นของบทนี้คือรู้ให้ชัดว่าแต่ละตัว coverage สิ้นสุดตรงไหน เพื่อให้รู้ว่าอะไรยังเป็นหน้าที่ของคุณอยู่
ruff — ไบนารีเดียว ทั้ง lint และ format จับ pattern ได้มากที่สุดฟรี
หัวข้อที่มีชื่อว่า “ruff — ไบนารีเดียว ทั้ง lint และ format จับ pattern ได้มากที่สุดฟรี”ruff check รันกฎหลายร้อยข้อ (reimplementation ของมันเองจาก pyflakes, pycodestyle, flake8-bugbear, security check ของ bandit, flake8-async และอื่น ๆ) ในรอบเดียวที่เร็ว โดยไม่ต้องติดตั้ง plugin แยก เก้าใน 24 pattern ของ catalog นี้ถูกจับโดย rule ของ ruff โดยไม่ต้องตั้งค่าอะไรเลย: B006, B023, TRY201, S101, RUF006, ASYNC251, PLW1641, RUF012, DTZ003, SIM115, S602, T201 บวก coverage บางส่วนจาก F632 และตระกูล E722/S110/BLE001 สำหรับ pattern #6
ruff --fix ปลอดภัยโดยออกแบบ — --unsafe-fixes ไม่ปลอดภัย --fix ใช้แค่ rewrite ที่ ruff มั่นใจว่ารักษาพฤติกรรมเดิม --unsafe-fixes ใช้ rewrite ที่ปกติถูกต้องแต่สามารถเปลี่ยนความหมายได้ — และมันไม่ถามก่อนทำ before/after จริงบน catalog pattern #1:
# beforedef add_item(item: str, cart: list[str] = []) -> list[str]: cart.append(item) return cartruff check --select B006 --fix --unsafe-fixes add_item.py# after -- ruff's own unsafe rewritedef add_item(item: str, cart: list[str] = None) -> list[str]: if cart is None: cart = [] cart.append(item) return cartการ rewrite นี้แก้ runtime bug จริง — แต่มันปล่อยให้ parameter ถูก annotate เป็น list[str] ในขณะที่ default เป็น None ซึ่งไม่ใช่ทางแก้เดียวกับที่คนที่รอบคอบจะเขียน (list[str] | None = None) การรัน mypy --strict บนไฟล์ ที่ rewrite แล้ว โดยไม่แก้ไขอะไรเพิ่ม:
error: Incompatible default for parameter "cart" (default has type "None", parameter has type "list[str]") [assignment]note: PEP 484 prohibits implicit Optional. Accordingly, mypy has changed its default to no_implicit_optional=True--unsafe-fixes แลก bug จริงหนึ่งตัวกับ type error จริงหนึ่งตัว แบบเงียบ ๆ ในรอบอัตโนมัติเดียว รัน type checker ใหม่เสมอ (และอ่าน diff) หลัง --unsafe-fixes — ห้ามรันมันแบบไม่มีคนดูใน CI เด็ดขาด
mypy --strict เทียบกับ pyright — เห็นตรงกันจริง และเห็นต่างกันจริง
หัวข้อที่มีชื่อว่า “mypy --strict เทียบกับ pyright — เห็นตรงกันจริง และเห็นต่างกันจริง”ทั้งคู่เป็น static type checker ที่อ่าน annotation เดียวกัน และมักเห็นตรงกัน — แต่ “มักจะ” ไม่ใช่ “เสมอ” และผู้รีวิวที่รันแค่ตัวเดียวอาจถูกหลอกโดยจุดบอดของอีกตัว
เห็นตรงกันจริง บน catalog pattern #17 (Optional ไม่ narrow):
mypy --strict: error: Item "None" of "str | None" has no attribute "upper" [union-attr]pyright: error: "upper" is not a known attribute of "None" (reportOptionalMemberAccess)เห็นตรงกันจริง บน catalog pattern #18 (list invariance):
mypy --strict: error: Argument 1 to "add_a_cat" has incompatible type "list[Dog]"; expected "list[Animal]" [arg-type]pyright: error: Argument of type "list[Dog]" cannot be assigned to parameter "animals" of type "list[Animal]" (reportArgumentType)เห็นต่างกันจริง บน status check บรรทัดเดียวที่โค้ด AI เขียนบ่อยมาก:
def is_ready(status: int) -> bool: return status == "ready"mypy --strict: error: Non-overlapping equality check (left operand type: "int", right operand type: "Literal['ready']") [comparison-overlap]pyright: 0 errors, 0 warnings, 0 informationsmypy --strict รวม --strict-equality ซึ่ง flag การเทียบระหว่างสอง type ที่ไม่มีทางเท่ากันได้จริง — status เป็น int เสมอ "ready" เป็น str เสมอ ดังนั้น status == "ready" เป็น False เสมอ และแทบจะแน่นอนว่าเป็น bug (เทียบตัวแปรผิด หรือเทียบกับ literal ผิด) pyright ไม่มี check เทียบเท่าเปิด default และเงียบสนิทกับไฟล์นี้เป๊ะ ถ้ารันแค่ pyright ใน CI จะปล่อย bug นี้ผ่านไป ถ้ารันแค่ mypy --strict จะจับได้
เงียบสนิทจริง ทั้งสองเครื่องมือ ไฟล์เดียวกัน บน catalog pattern #16 (Any บวก # type: ignore แบบเหมารวม):
mypy --strict: Success: no issues found in 1 source filepyright: 0 errors, 0 warnings, 0 informationsไม่มีเครื่องมือไหนพูดอะไรเกี่ยวกับ AttributeError ตอน runtime ที่รออยู่บรรทัดถัดไปเลย — Any เป็นทางออกที่จงใจของทั้งสอง type system ไม่ใช่ช่องโหว่ใน implementation ของตัวไหนเลย
python -X dev / -W error — เครื่องมือเดียวที่เฝ้าดูโปรแกรมรันจริง
หัวข้อที่มีชื่อว่า “python -X dev / -W error — เครื่องมือเดียวที่เฝ้าดูโปรแกรมรันจริง”python3.14 -X dev เปิด development mode ของ Python: assertion เพิ่มเติมตอน runtime, ResourceWarning สำหรับไฟล์ที่ไม่ปิด, asyncio debug mode (log slow-callback) และ — สำคัญที่สุดสำหรับ catalog นี้ — ข้อความ RuntimeWarning: coroutine '...' was never awaited ทันทีที่ coroutine ที่ถูกทิ้งไว้ถูก garbage-collect -W error::RuntimeWarning (ที่ verifier ของคอร์สนี้ใช้เอง) เปลี่ยน warning นั้นให้กลายเป็น exception ที่ raise ขึ้นมาเพิ่มเติม ทุกที่ที่ยังไม่ได้อยู่ใน warnings.catch_warnings() block
output จริง catalog pattern #9 (coroutine ที่ไม่เคย await):
Exception ignored while finalizing coroutine <coroutine object greet at 0x...>:RuntimeWarning: coroutine 'greet' was never awaitedoutput จริง catalog pattern #19 (datetime.utcnow()):
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).พลาด: ทุกอย่างที่ต้องการให้ code path นั้นรันจริง ๆ ระหว่างการรันนั้น coroutine ที่ไม่เคย await แต่ไม่เคยถูก garbage-collect ระหว่าง script สั้น ๆ หรือ race condition ที่บังเอิญไม่สลับผิดจังหวะในรอบนี้ ไม่ให้ warning อะไรเลย — -X dev พิสูจน์ว่ามีปัญหาที่มันสังเกตเห็น ไม่เคยพิสูจน์การไม่มีปัญหาที่มันบังเอิญไม่ trigger
pytest + coverage — บอกว่าอะไรไม่เคยถูกรัน ไม่ใช่บอกว่าอะไรพัง
หัวข้อที่มีชื่อว่า “pytest + coverage — บอกว่าอะไรไม่เคยถูกรัน ไม่ใช่บอกว่าอะไรพัง”def withdraw(balance: int, amount: int) -> int: if amount > balance: raise ValueError("insufficient funds") return balance - amountdef test_withdraw_success(): assert withdraw(100, 30) == 70output จริง pytest --cov=account --cov-report=term-missing:
. [100%]Name Stmts Miss Cover Missing------------------------------------------account.py 4 1 75% 3------------------------------------------TOTAL 4 1 75%1 passed in 0.01stest ตัวเดียวผ่าน และ coverage เป็น 75% — 25% ที่ขาดไปคือบรรทัด 3 พอดี ซึ่งคือ branch raise ValueError coverage ไม่ได้เจอ bug ในบรรทัดนั้น มันแค่เจอว่าไม่มีใคร test มันเลย ซึ่งเป็นการรับประกันที่ต่างกัน (และอ่อนกว่า)
พลาด: ความถูกต้องทั้งหมด coverage 100% พิสูจน์ว่าทุกบรรทัดถูกรันอย่างน้อยหนึ่งครั้งระหว่าง test ใดก็ได้ — มันไม่พิสูจน์อะไรเลยว่า assertion ใน test นั้นถูกต้องหรือเปล่า หรือ input เฉพาะที่ trigger bug (เช่น race ใน catalog pattern #12) เคยถูกลองหรือเปล่า ดู Worked Review 4 สำหรับ suite เต็มที่ผ่านด้วย coverage สูงทั้งที่มี bug จริงหลายตัวหลุดไป
bandit — static analysis เน้น security overlap กับ S rule ของ ruff โดยตั้งใจ
หัวข้อที่มีชื่อว่า “bandit — static analysis เน้น security overlap กับ S rule ของ ruff โดยตั้งใจ”bandit มีมาก่อน ruff จะ port security rule มาและยังมีประโยชน์เป็นความเห็นที่สองที่เป็นอิสระ หรือในโปรเจกต์ที่ยังใช้ ruff ไม่ได้ บน catalog นี้มัน flag ปัญหาเดียวกันกับที่ ruff’s S-prefixed rule ทำ ภายใต้ code หมายเลข B ของตัวเอง:
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.>> Issue: [B602:subprocess_popen_with_shell_equals_true] subprocess call with shell=True identified, security issue.>> Issue: [B404:blacklist] Consider possible security implications associated with the subprocess module.พลาด: อะไรก็ตามนอกเหนือจาก rule set ที่เน้น security ของมัน logic bug, mutability bug และ async pattern ส่วนใหญ่ใน catalog นี้อยู่นอกขอบเขตของ bandit โดยการออกแบบ
pip-audit (หรือเครื่องมือเทียบเท่าของ uv) — CVE ที่รู้จักใน version ที่ pin ไว้จริง
หัวข้อที่มีชื่อว่า “pip-audit (หรือเครื่องมือเทียบเท่าของ uv) — CVE ที่รู้จักใน version ที่ pin ไว้จริง”requirements.txt:requestsurllib3==1.26.4output จริงจาก pip-audit -r requirements.txt:
Found 18 known vulnerabilities in 1 packageName Version ID Fix Versions------- ------- --------------- -------------urllib3 1.26.4 PYSEC-2021-108 1.26.5urllib3 1.26.4 PYSEC-2023-192 1.26.17,2.0.6urllib3 1.26.4 PYSEC-2023-212 1.26.18,2.0.7...พลาด: โค้ดของคุณเองทั้งหมด pip-audit ตอบคำถามเดียวเท่านั้น — “version ที่ pin ไว้มีช่องโหว่ที่รู้จักและเผยแพร่แล้วหรือเปล่า” — และไม่มีอะไรพูดเกี่ยวกับ logic bug, concurrency bug หรือการใช้ API ผิด ไม่ว่าจะอยู่ในโค้ดของคุณเองหรือช่องโหว่ที่ยังไม่ถูกค้นพบใน dependency มันยังจับ requirement ที่ ไม่ pin ที่ค่อย ๆ เก่าไปในทิศทางตรงข้ามไม่ได้ หรือ sudo pip install ที่ทำให้ system Python สกปรก — ดู catalog pattern #23
🐛 หา bug ในโค้ดนี้
django>=4.0เฉลย
ไม่มีอะไรผิด syntax ที่นี่เลย และ pip-audit อาจรายงานช่องโหว่ที่รู้จักเป็นศูนย์กับอะไรก็ตามที่ >=4.0 resolve ไปวันนี้ นั่นแหละคือกับดัก: lower bound ที่ไม่ pin หมายความว่า ไฟล์เดียวกันเป๊ะ สามารถ resolve เป็น major version ใหม่ที่ต่างออกไปในเดือนหน้า โดยไม่จำเป็นต้องมีช่องโหว่เกี่ยวข้องเลย — แค่พฤติกรรมที่เปลี่ยนไปโดยไม่มีใครขอตอน install uv lock (หรือ pip-compile) เปลี่ยนสิ่งนี้ให้เป็น pin ที่แน่นอนและ reproducible ได้ รัน audit tool ใหม่ทุกครั้งที่ lockfile อัปเดต ไม่ใช่แค่ครั้งเดียว
รันทั้งชุดในคำสั่งเดียว
หัวข้อที่มีชื่อว่า “รันทั้งชุดในคำสั่งเดียว”แต่ละเครื่องมือครอบคลุมส่วนที่ต่างกันของ catalog นี้ ไม่ overlap กัน เมื่อ chain ด้วย && คำสั่งจะหยุดที่เครื่องมือแรกที่เจออะไรบางอย่าง — output จริง รันกับไฟล์ของ catalog pattern #17 หยุดที่ mypy:
ruff check . && mypy --strict . && pyright . && bandit -q -r . && pytest --covAll checks passed!file.py:2: error: Item "None" of "str | None" has no attribute "upper" [union-attr]Found 1 error in 1 file (checked 1 source file)ruff ผ่าน mypy --strict จับ bug ได้และ chain หยุดตรงนั้น — พฤติกรรม fail-fast แบบที่ต้องการพอดีใน pre-commit hook หรือ CI gate เพิ่ม pip-audit -r requirements.txt เป็นขั้นตอนแยกที่รันเป็นระยะ (ช่องโหว่ของ dependency เปลี่ยนไปตามเวลาแม้โค้ดของคุณจะไม่เปลี่ยนก็ตาม)
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TB subgraph Static["Static (source only)"] direction TB S1["Mutability/errors/style bugs: ruff (B006, B023, TRY201, S101, RUF006, ASYNC251, PLW1641, RUF012, DTZ003, SIM115, S602, T201, F632*)"] S2["Security-specific: bandit (overlaps ruff S-rules independently)"] S3["Type mismatches: mypy --strict, pyright (usually agree; can disagree; both silent on bare Any)"] S4["Known CVEs in pinned dependencies: pip-audit / uv audit"] end subgraph Dynamic["Dynamic (must run)"] direction TB D1["Runtime-only warnings: python -X dev / -W error (never-awaited, deprecated calls)"] D2["What ran vs. didn't: pytest --cov"] end