Stdlib, Testing และ Tooling
Batteries included
หัวข้อที่มีชื่อว่า “Batteries included”Python มาพร้อมกับปรัชญาที่เรียกว่า “batteries included”: standard library มีการ implement พร้อมใช้สำหรับงานที่โปรแกรมส่วนใหญ่ต้องการ — โดยไม่ต้องพึ่ง third-party package เมื่อติดตั้ง Python คุณจะได้รับ module สำหรับ collections, การจัดการข้อมูล, networking, file I/O, testing, serialisation, concurrency และอีกมากมายโดยอัตโนมัติ
Standard library ครอบคลุมมากกว่า 200 module
ก่อนที่จะใช้ pip ให้ตรวจสอบก่อนว่า stdlib module แก้ปัญหาของคุณได้หรือไม่ — จะ import ได้เร็วกว่า, เข้ากันได้กับ Python version ของคุณเสมอ และได้รับความเชื่อถือจากผู้ใช้นับล้าน
แนวทาง Python tooling สมัยใหม่
หัวข้อที่มีชื่อว่า “แนวทาง Python tooling สมัยใหม่”โปรเจกต์ Python สำหรับ production มักพึ่งพา tool ชุดเล็กๆ:
uv— เครื่องมือเดียวที่จัดการ interpreter, virtual environment, dependency และ lockfile (บทเรียนvenv-pip)pyproject.toml— ไฟล์สมัยใหม่สำหรับประกาศ package metadata, build backend และ tool configuration: ตาราง[build-system]มาจาก PEP 518, ตาราง[project](metadata) มาจาก PEP 621 ส่วน PEP 517 คือ build-backend interface ที่ tool เหล่านี้ implement (บทเรียนpackaging)pytest— test runner มาตรฐานโดยพฤตินัยสำหรับโปรเจกต์ Python ซึ่งสร้างบนunittestของ stdlib ด้วย API ที่เป็นมิตรกว่า (บทเรียนpytest)ruff— linter และ formatter ที่แทนที่flake8+black+isortด้วยเครื่องมือเดียวที่เร็วมาก อธิบายต่อในบทนี้mypy/pyright— static type checker ที่ตรวจสอบ type hint โดยไม่ต้องรันโค้ดจริง อธิบายต่อในบทนี้เช่นกัน
คุณจะได้สำรวจแต่ละตัวในบทเรียนของ chapter นี้ ตอนนี้ นิสัยสำคัญคือ: ใช้ stdlib ก่อนเสมอ
ลิ้มลองสั้นๆ: stdlib สามโมดูลในการใช้งานจริง
หัวข้อที่มีชื่อว่า “ลิ้มลองสั้นๆ: stdlib สามโมดูลในการใช้งานจริง”Demo ด้านล่างใช้สามโมดูลที่คุณจะพบบ่อยมาก:
collections.Counter— นับ hashable object ในรอบเดียวitertools.islice— ตัด iterator ใดก็ได้อย่าง lazy โดยไม่ต้อง materialize ก่อนjson— แปลง Python object เป็น JSON string และแปลงกลับ
import collectionsimport itertoolsimport json
# Count word frequencieswords = "the quick brown fox jumps over the lazy dog the fox".split()counter = collections.Counter(words)print("Top 3 words:", counter.most_common(3))
# First 5 squares using itertoolssquares = list(itertools.islice((x*x for x in range(1, 100)), 5))print("First 5 squares:", squares)
# JSON round-trippayload = json.dumps({"lang": "Python", "batteries": True})data = json.loads(payload)print("Parsed:", data["lang"], "batteries:", data["batteries"])Loading Python runtime (first run only)…
Linting และ formatting ด้วย ruff
หัวข้อที่มีชื่อว่า “Linting และ formatting ด้วย ruff”ruff คือ linter และ formatter ในตัวเดียว เร็วพอที่จะรันได้ทุกครั้งที่ save มัน implement rule ของ flake8, isort, pyupgrade, bugbear และอื่น ๆ ซ้ำ ภายใต้ rule code สั้น ๆ rule ส่วนหนึ่งจับ bug ที่ AI assistant สร้างบ่อยที่สุดได้พอดี
# ai_code.py -- every line below trips a real ruff ruleimport datetimeimport osimport sysfrom typing import List, Optional
def add_item(item, items=[]): items.append(item) return items
def make_report(created_at=datetime.datetime.now()) -> None: print(created_at)
def divide(a, b): try: return a / b except: pass
def get_names() -> Optional[List[str]]: result = os.getcwd() return NoneOutput จริง, ruff 0.16.8, rule set ดีฟอลต์ — ไม่ต้อง config อะไรเลย:
$ uvx ruff check ai_code.pyF401 [*] `sys` imported but unused --> ai_code.py:3:8
UP035 `typing.List` is deprecated, use `list` instead --> ai_code.py:4:1
B006 Do not use mutable data structures for argument defaults --> ai_code.py:7:26help: Replace with `None`; initialize within function
DTZ005 `datetime.datetime.now()` called without a `tz` argument --> ai_code.py:12:28
B008 Do not perform function call `datetime.datetime.now` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable --> ai_code.py:12:28
E722 Do not use bare `except` --> ai_code.py:19:5
S110 `try`-`except`-`pass` detected, consider logging the exception --> ai_code.py:19:5
UP045 [*] Use `X | None` for type annotations --> ai_code.py:23:20
UP006 [*] Use `list` instead of `List` for type annotation --> ai_code.py:23:29
F841 Local variable `result` is assigned to but never used --> ai_code.py:24:5
Found 10 errors.[*] 3 fixable with the `--fix` option (2 hidden fixes can be enabled with the `--unsafe-fixes` option).F401/F841 จับ dead code (import/variable ที่ไม่ได้ใช้) — ซากที่เหลือจาก AI assistant แก้ function แล้วทิ้ง import เก่าหรือตัวแปรกลางที่ไม่ได้ใช้ไว้ B006/B008 จับ bug ของ function signature ที่ AI สร้างบ่อยที่สุด: default argument ที่เป็น mutable หรือถูก evaluate สด ๆ ซึ่งคำนวณแค่ ครั้งเดียว ตอน import/definition แล้วถูกแชร์ข้ามทุกการเรียก E722 จับ except: เปล่าที่กลืนทุก exception รวมถึงตัวที่แทบไม่มีใครอยากกลืน (KeyboardInterrupt, SystemExit) กลุ่ม UP ชี้โค้ดที่เขียนตาม Python เวอร์ชันเก่า (typing.List, typing.Optional) ที่มีรูปแบบสั้นกว่าและเป็นปัจจุบันกว่า (list, X | None) — ตรงกับสไตล์ที่โมเดล AI ที่เทรนจากโค้ดเก่ามักจะใช้เป็นดีฟอลต์
ruff format (เป็นอีก subcommand แยกจาก ruff check) จัดการ whitespace/style แบบเดียวกับ black — การรันแค่ตัวใดตัวหนึ่งเป็นการตั้งค่าผิดที่พบบ่อย อธิบายต่อในหัวข้อ pre-commit ข้างล่าง
Type checking ใน CI: mypy --strict เทียบกับ pyright
หัวข้อที่มีชื่อว่า “Type checking ใน CI: mypy --strict เทียบกับ pyright”ทั้ง mypy และ pyright อ่าน type hint ชุดเดียวกันแต่สามารถเห็นต่างกันได้ ให้รันทั้งคู่ใน CI — bug ที่มีแค่ตัวเดียวจับได้ก็ยังเป็น bug จริง
def first_or_none(items: list[int]) -> int | None: if items: return items[0]
def total(values: list[int]) -> int: result = 0 for v in values: result += v return result
print(total(first_or_none([1, 2, 3])))$ uvx mypy --strict typing_demo.pytyping_demo.py:1: error: Missing return statement [return]typing_demo.py:13: error: Argument 1 to "total" has incompatible type "int | None"; expected "list[int]" [arg-type]Found 2 errors in 1 file (checked 1 source file)
$ uvx pyright typing_demo.pytyping_demo.py:13:13 - error: Argument of type "int | None" cannot be assigned to parameter "values" of type "list[int]" in function "total" Type "int | None" is not assignable to type "list[int]" "int" is not assignable to "list[int]" (reportArgumentType)1 error, 0 warnings, 0 informationsทั้งคู่จับ bug จริงได้ — การส่ง int | None เข้าที่ที่ต้องการ list[int] (crash จริง: การเรียก total ด้วย None จะ raise TypeError ตอน runtime) แต่ทั้งคู่ เห็นต่างกัน ในเรื่อง function แรก: mypy --strict ชี้เพิ่มว่า “Missing return statement” บน first_or_none ทั้งที่ return type ที่ประกาศไว้มี None รวมอยู่แล้ว (การตกออกจากท้าย function คือ การ return None ซึ่งเป็นค่าที่ valid ของ int | None); pyright ไม่ชี้ปัญหานี้เลย ไม่มีคำตัดสินไหนที่ “ผิด” — ทั้งสองใช้ heuristic ความเข้มงวดต่างกันกับ implicit-None return — แต่ทีมที่รันแค่ pyright ใน CI จะไม่เห็น warning แรกนั้นเลย และทีมที่รันแค่ mypy --strict ก็ได้ warning ที่บางคนมองว่าเป็น false positive บทเรียนคือ: รันทั้งคู่ถ้า CI time ไหว และรู้ว่าตัวไหนที่ gate การ merge ของคุณจริง ๆ
อัตโนมัติการตรวจสอบ: pre-commit
หัวข้อที่มีชื่อว่า “อัตโนมัติการตรวจสอบ: pre-commit”pre-commit รันชุดการตรวจสอบทุกครั้งที่ git commit เพื่อให้ bug ที่ ruff จับได้ไม่มีทางไปถึงแม้แต่ commit ในเครื่อง อย่าว่าแต่ CI เลย
repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.8 hooks: - id: ruff-check args: [--fix] - id: ruff-formatruff-check และ ruff-format เป็นสอง hook แยกกัน — config ที่มีแค่ตัวเดียวจะข้ามการตรวจของอีกตัวไปแบบเงียบ ๆ ลำดับสำคัญเมื่อใช้ --fix: ruff-check รันก่อน ruff-format เพื่อให้การเขียนใหม่ที่ ruff --fix ทำเพื่อแก้ lint rule ถูก format ต่อทีหลัง ไม่ถูกทิ้งไว้แบบ format ครึ่งเดียว
จับ bug ตอน runtime: python -X dev และ -W error
หัวข้อที่มีชื่อว่า “จับ bug ตอน runtime: python -X dev และ -W error”บาง bug จะเงียบโดยดีฟอลต์เพราะ warning filter ดีฟอลต์ของ Python ซ่อนมันไว้ -X dev (“development mode”) เปิดหลายตัวในนั้น
def read_first_line(path: str) -> str: f = open(path) # BUG: never closed, no 'with' return f.readline()
read_first_line(__file__)print("done")$ python3.14 devdemo2.pydone
$ python3.14 -X dev devdemo2.pydevdemo2.py:6: ResourceWarning: unclosed file <_io.TextIOWrapper name='devdemo2.py' mode='r' encoding='UTF-8'> read_first_line(__file__)ResourceWarning: Enable tracemalloc to get the object allocation tracebackdoneการรันเปล่า ๆ รั่ว file handle แบบเงียบ ๆ; -X dev เปิดการรายงาน ResourceWarning (พร้อมการตรวจอื่น ๆ: assertion เพิ่มเติมใน interpreter, PYTHONMALLOC=debug, คำเตือนของ io.IOBase.__del__) และพิมพ์ตำแหน่งที่มันเกิดขึ้นเป๊ะ ๆ -W error::<category> ไปไกลกว่านั้นอีกขั้น โดยเปลี่ยน warning category ที่ระบุให้กลายเป็น hard failure — มีประโยชน์ใน CI เพื่อให้แน่ใจว่า deprecation ที่ไม่มีใครคอยดูจะไม่แอบผ่านไปเรื่อย ๆ จนถึงวันที่มันถูกลบจริง:
import datetime
def log_timestamp() -> None: now = datetime.datetime.utcnow() # deprecated since 3.12, naive UTC print(f"logged at {now}")
log_timestamp()print("done")$ python3.14 -W error::DeprecationWarning devdemo3.pyTraceback (most recent call last): File "devdemo3.py", line 9, in <module> log_timestamp() File "devdemo3.py", line 5, in log_timestamp now = datetime.datetime.utcnow() # deprecated since 3.12, naive UTCDeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled forremoval in a future version. Use timezone-aware objects to represent datetimesin UTC: datetime.datetime.now(datetime.UTC).Exit code จริง: 1 ถ้าไม่มี -W error โปรแกรมเดียวกันนี้จะพิมพ์ deprecation warning แล้วรันต่อจนจบ — พลาดง่ายมากใน CI log ที่ยุ่งเหยิง
บันไดการตรวจสอบ (verification ladder)
หัวข้อที่มีชื่อว่า “บันไดการตรวจสอบ (verification ladder)”แต่ละ tool ในโมดูลนี้จับ bug คนละประเภท เรียงจากถูกที่สุดและควรรันบ่อยที่สุด ไปหาแพงที่สุดและควรรันน้อยที่สุด:
flowchart TD A["ruff check + ruff format (milliseconds, every save)"] --> B["mypy --strict / pyright (seconds, every save or pre-commit)"] B --> C["pytest (seconds to minutes, every commit/CI run)"] C --> D["hypothesis-backed tests (slower, property-based, finds edge cases the other three miss)"]
ruff จับ bug ระดับ syntax และ style (dead code, default อันตราย, API ที่ deprecate แล้ว) โดยไม่ต้องรันโค้ดคุณเลย Type checker จับ bug เชิง logic ที่กว้างกว่า — type ผิดที่ไหลระหว่าง function — ก็ยังไม่ต้องรันอะไรเช่นกัน pytest รันโค้ดคุณจริง ๆ กับตัวอย่างที่คุณเลือกเอง hypothesis (อธิบายละเอียดในบทเรียน pytest) รันโค้ดคุณกับตัวอย่างที่ มันเอง เลือก โดยเจาะจงล่า input ที่คุณคิดไม่ถึง ไม่มีตัวไหนใน 4 ตัวนี้แทนที่กันได้ — โปรแกรมสามารถผ่าน ruff และ type checker ทั้งสองตัวพร้อม test coverage 100% แล้วยังผิดอยู่ได้ ถ้า assertion ของทุก test อ่อนเหมือน coverage demo ในบทเรียน pytest
บทเรียนในโมดูลนี้
หัวข้อที่มีชื่อว่า “บทเรียนในโมดูลนี้”pytest— fixture,parametrize, mocking, coverage และhypothesisแบบละเอียดstdlib-tour—datetime,json,functools,itertools,pathlib,subprocessและอื่น ๆvenv-pip—uvก่อน แล้วค่อยvenv/pipที่อยู่ข้างใต้packaging—pyproject.toml,uv build/uv publish, dependency groupregex—re: raw string,match/search/fullmatch/findall/finditer, group, flag, การแทนที่ (substitution) และ catastrophic backtrackinglogging— modulelogging: logger, handler, level,dictConfig, structured logging และ modulewarningsindex(บทนี้) —ruff, กลยุทธ์ type-checker ใน CI,pre-commit, runtime warning และวิธีที่ 4 ชั้นการตรวจสอบเชื่อมกัน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ทุก tool ใน chapter นี้ตอบคำถามเดียวกันที่จุดต่างกันใน development loop: “โค้ดนี้ถูกไหม และจะรู้ได้ถูกที่สุดยังไง” ตัว stdlib เองคือรูปแบบความถูกต้องที่ถูกที่สุด — โค้ดที่คุณไม่ได้เขียนเองไม่มี bug ที่คุณใส่เข้าไปได้ ruff มาต่อ: มันอ่านข้อความ source ของคุณและชี้ pattern ที่รู้ว่าทำให้เกิด bug โดยไม่รันโค้ดสักบรรทัดเดียว Type checker อ่านทั้ง source และ annotation ของคุณ แล้วจำลองว่า value ไหนไหลไปที่ไหนได้บ้าง ก็ยังไม่ต้องรันอะไร pytest และ hypothesis เป็น tool เดียวในบันไดนี้ที่รันโค้ดคุณจริง ๆ — ซึ่งก็เป็นเหตุผลว่าทำไมมันช้าที่สุดและเป็นด่านสุดท้าย ไม่ใช่ด่านแรก
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| ใช้ stdlib ก่อนเสมอ | ไม่เพิ่ม dependency, เวอร์ชัน หรือ security surface | บางครั้งยาวกว่า third-party convenience library |
ruff เป็น linter + formatter | เครื่องมือเร็วมากตัวเดียวแทน flake8 + black + isort | rule surface ดีฟอลต์+opt-in ใหญ่ต้องเรียนรู้; --fix อาจไม่ปลอดภัยสำหรับบาง rule |
รันทั้ง mypy --strict และ pyright ใน CI | จับ bug ของทั้งสอง tool รวมกัน; เห็นความเห็นต่าง | เวลา CI เพิ่มเป็นสองเท่า; บางครั้งต้องตัดสินความเห็นต่าง |
pre-commit | Bug ไม่มีทางถึง commit อย่าว่าแต่ CI | ต้องมี config ไฟล์เพิ่ม; hook ที่ตั้งค่าผิด (มีแค่ ruff-format ไม่มี ruff-check) ให้ความมั่นใจปลอม |
-X dev / -W error ใน CI หรือรันในเครื่อง | เผย resource leak และ deprecation แบบเงียบ ๆ ก่อนที่จะสำคัญ | รันช้าลงนิดหน่อย; บาง warning ต้อง filter ต่อโปรเจกต์เพื่อไม่ให้ noise จาก dependency |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
ruff formatกับruff checkคือคำสั่งเดียวกัน” ทั้งสองเป็น subcommand แยกกัน (และ hook id ของpre-commitแยกกัน คือruff-formatกับruff-check) — formatting แก้ whitespace/style ส่วน checking ชี้ bug และ anti-pattern จริง; hook หรือ CI step ที่รันแค่ตัวเดียวจะข้ามปัญหาทั้งหมวดของอีกตัวไปแบบเงียบ ๆ - “ถ้า
pyrightสะอาดmypy --strictก็ต้องสะอาดด้วย (หรือกลับกัน)” demo ของfirst_or_none/totalข้างบนคือหลักฐานจริงที่ตรวจสอบแล้วว่าไม่ใช่:mypy --strictรายงาน 2 error,pyrightรายงาน 1 error บนไฟล์เดียวกันเป๊ะ - “การรันแล้วเงียบ สะอาด แปลว่าไม่มี bug” demo unclosed-file รันจนจบโดยไม่มี error ให้เห็นภายใต้
python3.14 devdemo2.pyเปล่า ๆ — resource leak จะเห็นได้ก็ต่อเมื่ออยู่ภายใต้-X devเท่านั้น - “deprecation warning เป็นแค่ noise ที่เพิกเฉยได้จนกว่าจะถูกลบจริง”
-W error::DeprecationWarningในบทนี้เปลี่ยน warning ประเภทนั้นให้กลายเป็น failure ที่ดังและทันที — นั่นคือประเด็น: จับมันตอนที่ยังมีเวลาแก้อย่างใจเย็น ไม่ใช่วันที่ API ถูกลบจริง - “pip คือสิ่งแรกที่ควรใช้เมื่อต้องการ functionality” ตรวจ stdlib ก่อน (
collections,itertools,functools,pathlibฯลฯ — ดู Callout ข้างบน) ช่วยเลี่ยงการเพิ่ม dependency, เวอร์ชันที่ต้องติดตาม และความเสี่ยง supply-chain สำหรับสิ่งที่ standard library อาจทำได้อยู่แล้ว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อสคริปต์ setup, CI config หรือไฟล์ pre-commit ที่ generate มา ให้ตรวจ:
- config
pre-commitที่มีแค่ hookruff-format(หรือruffแบบเก่า) และไม่มีruff-check(หรือกลับกัน) — การตรวจทั้งหมวด (formatting หรือ linting) ถูกข้ามไปแบบเงียบ ๆ - CI ที่รันแค่
mypyหรือแค่pyrightแล้วบอกว่า “type checking เสร็จแล้ว” — ทั้งสอง tool เห็นต่างกันได้จริง ตามที่แสดงข้างบน - โค้ดที่ใช้
except:เปล่า แทนที่จะเป็นexcept Exception:หรือ exception type ที่ระบุชื่อ —E722ของruffจับสิ่งนี้ได้ แต่ก็ต่อเมื่อruff checkรันจริงใน CI ไม่ใช่แค่ในเครื่อง - Function ที่มี default argument เป็น mutable (
[],{}) หรือถูก evaluate สด ๆ (datetime.now()) —B006/B008ของruffจับสิ่งนี้ได้ แต่การรัน--fixโดยไม่ review อาจเปลี่ยนพฤติกรรมแบบเงียบ ๆ สำหรับ edge case ที่ fix ไม่ได้ทำซ้ำครบ - โปรแกรมที่ไม่เคย crash ในเครื่องแต่รั่ว file handle, thread หรือ connection — มองไม่เห็นถ้าไม่มี
-X devหรือ test ที่ track resource ชัดเจน
🐛 หา bug ในโค้ดนี้
repos:- repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.8 hooks: - id: ruff-formatเฉลย
Hook นี้รันแค่ ruff-format (whitespace/style) เท่านั้น — ไม่เคยรัน ruff-check (linter) เลย ดังนั้น bug ทุกตัวที่ demo ai_code.py ของบทนี้แสดง (B006, B008, E722, F401, F841, กลุ่ม UP) สามารถ commit และ push ได้ทั้งที่ pre-commit ผ่านเขียวหมด ทางแก้คือเพิ่ม hook entry ที่สองสำหรับ ruff-check (พร้อม args: [--fix] ถ้าต้องการ auto-fix) ก่อน entry ของ ruff-format ให้ตรงกับ config ที่ใช้งานได้จริงที่แสดงไว้ก่อนหน้าในบทนี้