ข้ามไปยังเนื้อหา

Exceptions

Exception คือสัญญาณที่บอกว่ามีบางอย่างผิดพลาดระหว่างการรัน เมื่อ Python raise exception จะ unwind call stack จนกว่าจะเจอ except clause ที่ตรงกัน หรือหยุดโปรแกรม

Exception คือ object — instance ของ class ที่สืบทอดจาก BaseException Exception ที่ใช้งานในโค้ดทั่วไปมักสืบทอดจาก Exception

Exception ทุกตัวคือ class และ exception class ทุกตัวสืบทอด — ทางตรงหรือทางอ้อม — จาก BaseException Exception คือแขนงหนึ่งของ tree นั้น ส่วนสัญญาณควบคุม process อีกไม่กี่ตัวอยู่คนละแขนงที่ Exception ไม่ครอบคลุม:

flowchart TD
BE["BaseException"] --> KI["KeyboardInterrupt"]
BE --> SE["SystemExit"]
BE --> GE["GeneratorExit"]
BE --> EXC["Exception"]
EXC --> VE["ValueError"]
EXC --> TE["TypeError"]
EXC --> LE["LookupError"]
LE --> KE["KeyError"]
LE --> IE["IndexError"]
EXC --> OSE["OSError"]
EXC --> RTE["RuntimeError"]
EXC --> EG["ExceptionGroup"]
ส่วนหนึ่งของ built-in exception hierarchy

นี่คือเหตุผลที่ except Exception, bare except: และ except BaseException เป็นสามสิ่งที่ต่างกัน:

  • except SpecificError: — จับเฉพาะ class นั้นและ subclass ของมัน ควรใช้แบบนี้เสมอ
  • except Exception: — จับทุกอย่างที่เป็น application-level error แต่ ไม่ จับ KeyboardInterrupt, SystemExit หรือ GeneratorExit — สิ่งเหล่านั้นเป็นสัญญาณควบคุม process ไม่ใช่ bug
  • bare except: — เหมือนกับ except BaseException: เป๊ะ มันจับทุกอย่างจริง ๆ รวมถึง Ctrl+C และ sys.exit() ที่สะอาด นี่แทบไม่ใช่สิ่งที่ต้องการเลย: มันทำให้โปรแกรมขัดจังหวะไม่ได้ และกลืนสัญญาณ shutdown ที่ตั้งใจให้ propagate ไปแบบเงียบ ๆ

การรวม except แบบกว้าง ๆ กับ body ที่ว่างเปล่าคือวิธีที่พบบ่อยที่สุดที่โค้ดที่ AI สร้างซ่อน bug จริงไว้หลังค่า fallback ที่ดูสมเหตุสมผล:

def risky_lookup(data: dict, key: str) -> int:
try:
return data[key]
except: # catches EVERYTHING, not just KeyError
pass
return -1

key ที่ไม่มีอยู่จะคืน -1 — ซึ่งน่าจะเป็นสิ่งที่ตั้งใจไว้ แต่การเรียก risky_lookup(None, "x") ก็เช่นกัน ซึ่งจะ raise TypeError: 'NoneType' object is not subscriptable — เป็น programming error จริง ไม่ใช่ key ที่หาย — และถูกกลืนไปแบบเดียวกันเป๊ะ คืนค่า -1 ราวกับไม่มีอะไรเกิดขึ้น ทางแก้คือระบุ exception ที่คาดไว้ให้เจาะจงเสมอ (except KeyError:) เพื่อให้ exception อื่น ยังคงโผล่ออกมา

# illustrative — names below are placeholders
try:
result = risky_operation()
except SomeError as e:
# runs only if SomeError (or a subclass) was raised
handle(e)
else:
# runs only if NO exception was raised in try
use(result)
finally:
# ALWAYS runs — exception or not
cleanup()

Block else เป็น feature Python ที่มีประโยชน์: ช่วยแยกแยะ “การดำเนินการสำเร็จ” ออกจาก “เราอยู่ใน cleanup” โค้ดใน else ทำงานเฉพาะเส้นทาง happy path เท่านั้น

return, break หรือ continue ใน finally จะทิ้ง exception ที่กำลังลอยอยู่ทั้งหมด — function จะ return ตามปกติและ error ก็หายไปเฉย ๆ Python 3.14 (PEP 765) เตือนกรณีนี้แล้ว:

def risky():
try:
raise ValueError("lost")
finally:
return "swallowed" # SyntaxWarning in 3.14 — the ValueError never escapes

ให้ finally ทำแค่ cleanup แล้วปล่อยให้ exception ไหลต่อไป

จับ exception ที่เฉพาะเจาะจงที่สุดเสมอ การจับ Exception เป็นทางเลือกสุดท้าย ส่วน except: เปล่าจะกลืน KeyboardInterrupt และ SystemExit ไปด้วย

try:
n = int(user_input)
except ValueError:
print("Not a valid integer")
except TypeError:
print("Expected a string, not None")

จับหลายประเภทใน clause เดียวด้วย tuple:

except (ValueError, TypeError) as e:
print(f"Input error: {e}")

ตั้งแต่ Python 3.14 (PEP 758) วงเล็บเป็นตัวเลือกได้เมื่อ clause ไม่มี as:

except ValueError, TypeError:
print("Input error")

ถ้ามี as ยังต้องใส่วงเล็บอยู่ — except (ValueError, TypeError) as e: — เพราะไม่งั้น parser จะแยกไม่ออกว่าเป็น tuple หรือ binding

วัฒนธรรม Python ชอบ EAFP (“easier to ask forgiveness than permission” — ลองทำแล้วจับ failure) มากกว่า LBYL (“look before you leap” — เช็ค precondition ก่อนแล้วค่อยทำ):

def lbyl(d: dict[str, int], key: str) -> int:
if key in d: # check
return d[key] # use -- something could remove `key` between these two lines
return 0
def eafp(d: dict[str, int], key: str) -> int:
try:
return d[key] # single operation -- no gap for anything else to run in between
except KeyError:
return 0

ทั้งสองแบบให้คำตอบเหมือนกันสำหรับ dict ธรรมดาแบบ single-threaded ความต่างจะสำคัญขึ้นทันทีที่ thread อื่น, signal handler หรือจุด await สามารถทำงานคั่นระหว่างการ check กับการ use ได้ — LBYL มีช่องโหว่แบบ TOCTOU (“time-of-check to time-of-use”) ที่เงื่อนไขที่เช็คไว้อาจกลายเป็นเท็จก่อนที่จะลงมือทำ ตัวอย่างจริงแบบคลาสสิก: if os.path.exists(path): os.remove(path) ยังสามารถ raise FileNotFoundError ได้ถ้า process อื่นลบไฟล์นั้นไปก่อน EAFP ที่เป็น operation เดียวไม่มีช่องโหว่แบบนี้

Use raise to signal an error explicitly:

def divide(a: float, b: float) -> float:
if b == 0:
raise ValueError("divisor cannot be zero")
return a / b

การ re-raise exception เดียวกัน ใน except block ดูเหมือนใช้แทนกันได้ระหว่าง bare raise กับ raise e แต่ไม่ใช่:

def a():
b()
def b():
raise ValueError("deep failure")
def reraise_bare():
try:
a()
except ValueError:
raise # preserves the original traceback -- adds no frame
def reraise_e(err: ValueError):
raise err # adds a frame pointing at THIS line

การเรียก reraise_bare() ให้ traceback ที่จบที่ b() เหมือนเดิม การเรียก a(), จับ, แล้วเรียก reraise_e(caught) เพิ่ม frame เข้าไปอีกหนึ่งใน traceback — บรรทัดข้างใน reraise_e — ถึงแม้จะเป็น exception object “ตัวเดียวกัน” ก็ตาม ยืนยันได้โดยนับจำนวน frame ของ traceback.extract_tb ใน demo ที่รันได้ด้านล่าง: bare raise คงจำนวน frame เดิม; raise e เพิ่มมาหนึ่ง ควรใช้ bare raise เมื่อ re-raise แบบไม่เปลี่ยนแปลง — มันคง traceback ให้ชี้ไปที่จุดกำเนิดจริง

เมื่อจับ exception หนึ่งแล้ว raise อีกตัว ใช้ raise NewError(...) from original เพื่อเก็บตัวเดิมไว้เป็น cause การทำแบบนี้เก็บ traceback เต็มไว้ใน error chain

try:
value = int(raw_string)
except ValueError as original:
raise RuntimeError("Failed to parse config") from original

attribute __cause__ บน exception ใหม่เก็บตัวเดิมไว้ Python แสดง traceback ทั้งสองให้อัตโนมัติ

ถ้าต้องการทิ้ง cause อย่างชัดเจน (suppress chained traceback) ใช้ raise ... from None ถ้าไม่มีทั้ง from original และ from None การ raise exception ใหม่ใน except block ยังคงบันทึกตัวเดิมไว้ใน __context__ (implicit chaining ซึ่งอยู่ในบท Custom Errors) — from None เป็นวิธีเดียวที่ suppress สิ่งนั้นได้ทั้งหมด

assert มีไว้สำหรับเช็คตัวเองภายในและ test ไม่ใช่สำหรับ validate ข้อมูลที่โปรแกรมของคุณไม่ได้ควบคุม การรัน Python ด้วย -O (optimized mode) จะเอา statement assert ทุกตัวออกจาก bytecode ที่ compile แล้วทั้งหมด — เงื่อนไขไม่ถูก evaluate เลยแม้แต่ครั้งเดียว:

def validate(value: int) -> int:
assert value > 0, "value must be positive"
return value
print(validate(-1))
$ python3.14 validate.py
Traceback (most recent call last):
...
AssertionError: value must be positive
$ python3.14 -O validate.py
-1

source เดียวกันเป๊ะให้ crash หรือรับเลขติดลบไปแบบเงียบ ๆ ขึ้นอยู่กับ flag ที่เลือกจากภายนอกไฟล์ทั้งหมด ใช้ if/raise (มักเป็น ValueError) สำหรับอะไรก็ตามที่ต้องถูกเช็คเสมอ และเก็บ assert ไว้สำหรับเงื่อนไขที่บ่งบอกว่า มี bug ใน โค้ดของคุณเอง ไม่ใช่ external input ที่ไม่ดี

บาง operation — concurrent task, batch validation — สามารถล้มเหลวด้วย exception ที่ไม่เกี่ยวข้องกันมากกว่าหนึ่งตัว พร้อมกัน ExceptionGroup รวม exception หลายตัวเป็น object เดียว except* แกะ group ออกโดยจับ exception แต่ละตัว ในนั้นเทียบกับ type ที่ระบุ อาจรัน handler มากกว่าหนึ่งตัวสำหรับการ raise ครั้งเดียว:

def run_tasks() -> None:
errors: list[Exception] = [ValueError("bad input"), TypeError("bad type")]
raise ExceptionGroup("task failures", errors)
try:
run_tasks()
except* ValueError as eg:
print(f"ValueError group: {[str(e) for e in eg.exceptions]}")
except* TypeError as eg:
print(f"TypeError group: {[str(e) for e in eg.exceptions]}")

ทั้ง except* ValueError และ except* TypeError ทำงานที่นี่ แต่ละตัวได้รับ sub-group ของตัวเอง except ValueError ธรรมดาจับ ExceptionGroup ไม่ได้เลย — มันไม่ match เว้นแต่จะระบุ ExceptionGroup เอง (หรือ superclass ของมัน) PEP 758 (3.14) คือการเปลี่ยน syntax ที่ทำให้ except และ except* clause ตัดวงเล็บรอบ type หลายตัวออกได้อย่างที่แสดงไว้ก่อนหน้าในบทนี้ มันไม่ได้เปลี่ยนวิธีที่ exception group เองทำงาน

สำหรับกรณีเฉพาะเจาะจงและถูกต้องของ “จงใจเมิน exception type นี้” contextlib.suppress ชัดเจนกว่า try/except/pass block เพราะมันระบุชัดว่ากำลัง suppress อะไรอยู่ ในบรรทัดเดียว:

import contextlib
with contextlib.suppress(FileNotFoundError):
open("does-not-exist.txt")
# execution continues here either way

ต่างจาก bare except: pass, suppress รับรายการ exception type ที่ชัดเจน — มันไม่มีทางกลืน KeyboardInterrupt หรือ TypeError ที่เกิดจาก typo แบบที่ except: ที่ไม่ระบุ type ทำได้โดยไม่ตั้งใจ

print(e) (หรือ print(f"{e}")) แสดงแค่ message ของ exception — traceback, chain ของ cause และบรรทัดที่แน่นอน หายไปหมด logging.exception(...) ที่เรียกจากภายใน except block บันทึก traceback เต็มให้อัตโนมัติ:

import logging
try:
1 / 0
except ZeroDivisionError as e:
print(f"print(e) shows only: {e}") # "division by zero" -- no location, no traceback
logging.exception("division failed") # full traceback, at ERROR level, plus your message

logging.exception เป็นตัวย่อของ logging.error(..., exc_info=True) — ต้องถูกเรียกระหว่างที่กำลัง handle exception นั้นอยู่ (ภายใน except block) เพื่อให้ exc_info=True มีอะไรให้แนบ

sys.exit(code) ไม่ได้บังคับหยุด process ทันที — มันจะ raise SystemExit ซึ่ง propagate เหมือน exception ทั่วไป (รัน finally block ระหว่างทางด้วย) เว้นแต่มีอะไรจับมันไว้:

import sys
def cli_entry(code: int) -> None:
sys.exit(code)
try:
cli_entry(2)
except SystemExit as e:
print(f"SystemExit caught, code={e.code}")

เพราะ SystemExit สืบทอดจาก BaseException ไม่ใช่ Exception ทำให้ except Exception: ไม่เคย intercept การเรียก sys.exit() ที่สะอาดได้เลย — มีแค่ bare except: หรือ except SystemExit:/except BaseException: ที่ชัดเจนเท่านั้นที่ทำได้ นี่เป็นความตั้งใจ: handler ระดับ except Exception ของ application ไม่ควรสามารถ ขวางไม่ให้โปรแกรม exit ได้

Traceback ของ Python แสดง frame จากบนลงล่างตามลำดับที่ เข้า มา แต่ลำดับที่อ่านแล้วมีประโยชน์คือจากล่างขึ้นบน: บรรทัดสุดท้ายบอกชื่อและ message ของ exception, frame เหนือขึ้นไปคือจุดที่ exception นั้น raise จริง ๆ, และแต่ละ frame ที่สูงขึ้นไปคือหนึ่งระดับที่ไกลออกไปใน call chain ที่นำไปสู่จุดนั้น

def outer():
middle()
def middle():
inner()
def inner():
raise ValueError("innermost failure")
outer()
Traceback (most recent call last):
File "demo.py", line 9, in <module>
outer()
File "demo.py", line 2, in outer
middle()
File "demo.py", line 5, in middle
inner()
File "demo.py", line 8, in inner
raise ValueError("innermost failure")
ValueError: innermost failure

เริ่มที่ ValueError: innermost failure (สิ่งที่พัง) แล้วดู frame ที่อยู่เหนือขึ้นไปทันที (inner จุดที่มันพัง) แล้วค่อยไล่ขึ้นผ่าน middle กับ outer เฉพาะเมื่อต้องรู้ว่า execution มาถึงจุดนั้นได้อย่างไร การอ่านจากบนลงล่าง จะล่อให้เริ่ม debug ที่ outer ทั้งที่มันไม่ได้พังจริง ๆ

def safe_divide(a: float, b: float) -> float:
"""Divide a by b, raising ValueError if b is zero."""
if b == 0:
raise ValueError("divisor cannot be zero")
return a / b
# --- try / except / else / finally ---
print("=== basic try/except/else/finally ===")
for b in [2.0, 0]:
try:
result = safe_divide(10.0, b)
except ValueError as e:
print(f" ValueError: {e}")
else:
print(f" Result: {result}")
finally:
print(" finally ran")
# --- catching specific types ---
print("\n=== specific exception types ===")
vals: list[str | None] = ["42", "abc", None]
for val in vals:
try:
if val is None:
raise TypeError("expected str, got None")
n = int(val)
print(f" parsed {val!r} -> {n}")
except (TypeError, ValueError) as e:
print(f" {type(e).__name__} for {val!r}: {e}")
# --- exception chaining ---
print("\n=== exception chaining ===")
try:
try:
raw = "not-a-number"
value = int(raw)
except ValueError as original:
raise RuntimeError("Failed to parse config value") from original
except RuntimeError as e:
print(f" RuntimeError: {e}")
print(f" __cause__: {e.__cause__}")
import contextlib
import logging
import sys
import traceback
# --- hierarchy: except Exception vs bare except vs BaseException ---
print("=== hierarchy ===")
for kind in (ValueError("bad value"), KeyboardInterrupt(), SystemExit(1)):
try:
raise kind
except Exception as e:
print(f" except Exception caught: {type(e).__name__}")
except BaseException as e:
print(f" except Exception missed it; only BaseException caught: {type(e).__name__}")
# --- except: pass swallows real bugs silently ---
print("=== except: pass ===")
def risky_lookup(data: dict, key: str) -> int:
try:
return data[key]
except: # deliberately bad: swallows EVERYTHING, including a bad argument type below
pass
return -1
print(f" risky_lookup({{}}, 'x') = {risky_lookup({}, 'x')}") # KeyError silently becomes -1
result = risky_lookup(None, "x") # TypeError: 'NoneType' is not subscriptable -- ALSO silently swallowed
print(f" risky_lookup(None, 'x') = {result} <- a real bug (wrong argument type) hidden as -1")
# --- raise vs raise e vs raise ... from ---
print("=== raise vs raise e ===")
def a():
b()
def b():
raise ValueError("deep failure")
def reraise_bare():
try:
a()
except ValueError:
raise # preserves the original traceback -- no new frame for this line
def reraise_e(err: ValueError):
raise err # adds THIS line as a new traceback frame
try:
reraise_bare()
except ValueError:
frames_bare = len(traceback.extract_tb(sys.exc_info()[2]))
print(f" bare raise traceback frames: {frames_bare}")
try:
try:
a()
except ValueError as caught:
reraise_e(caught)
except ValueError:
frames_e = len(traceback.extract_tb(sys.exc_info()[2]))
print(f" raise e traceback frames: {frames_e}")
# --- from / from None ---
try:
try:
int("not-a-number")
except ValueError as original:
raise RuntimeError("config parse failed") from original
except RuntimeError as e:
print(f" __cause__ set by 'from': {e.__cause__!r}")
try:
try:
int("not-a-number")
except ValueError:
raise RuntimeError("config parse failed") from None
except RuntimeError as e:
print(f" __cause__ suppressed by 'from None': {e.__cause__!r}")
# --- finally overriding return ---
print("=== finally overriding return ===")
def leaky() -> str:
try:
raise ValueError("lost forever")
finally:
return "swallowed" # SyntaxWarning in 3.14 (PEP 765) -- the ValueError never escapes
print(f" leaky() = {leaky()!r} <- no exception visible at all")
# --- EAFP vs LBYL ---
print("=== EAFP vs LBYL ===")
config: dict[str, int] = {"retries": 3}
def lbyl(d: dict[str, int], key: str) -> int:
if key in d:
return d[key]
return 0
def eafp(d: dict[str, int], key: str) -> int:
try:
return d[key]
except KeyError:
return 0
print(f" lbyl(config, 'retries') = {lbyl(config, 'retries')}")
print(f" eafp(config, 'retries') = {eafp(config, 'retries')}")
# --- except* and exception groups (3.11+) ---
print("=== except* / exception groups ===")
def run_tasks() -> None:
errors: list[Exception] = [ValueError("bad input"), TypeError("bad type")]
raise ExceptionGroup("task failures", errors)
try:
run_tasks()
except* ValueError as eg:
print(f" except* ValueError caught {len(eg.exceptions)}: {[str(e) for e in eg.exceptions]}")
except* TypeError as eg:
print(f" except* TypeError caught {len(eg.exceptions)}: {[str(e) for e in eg.exceptions]}")
# --- contextlib.suppress ---
print("=== contextlib.suppress ===")
with contextlib.suppress(FileNotFoundError):
open("does-not-exist.txt")
print(" suppress(FileNotFoundError) let execution continue past the failed open()")
# --- logging.exception vs print(e) ---
print("=== logging.exception vs print(e) ===")
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format=" LOG: %(message)s")
try:
1 / 0
except ZeroDivisionError as e:
print(f" print(e) shows only: {e}")
logging.exception("division failed") # includes the full traceback, not just the message
# --- sys.exit / SystemExit ---
print("=== sys.exit / SystemExit ===")
def cli_entry(code: int) -> None:
sys.exit(code)
try:
cli_entry(2)
except SystemExit as e:
print(f" SystemExit caught, code={e.code} -- inherits BaseException, not Exception")
# --- assert is stripped by -O ---
print("=== assert and -O ===")
def validate(value: int) -> int:
assert value > 0, "value must be positive" # removed entirely when Python runs with -O
return value
print(f" validate(5) = {validate(5)}")
try:
validate(-1)
except AssertionError as e:
print(f" validate(-1) raised AssertionError: {e} <- gone under -O, never validated")
# --- reading a traceback bottom-up ---
print("=== reading a traceback bottom-up ===")
def outer():
middle()
def middle():
inner()
def inner():
raise ValueError("innermost failure")
try:
outer()
except ValueError:
tb_text = traceback.format_exc()
print(" " + tb_text.strip().splitlines()[-1])
print(" ^ read bottom-up: this line names the error; frames above show the call chain that led here")

การเดินทางของ exception คือเส้นตรงเส้นเดียวขึ้นไปตาม call stack: raise สร้างมันขึ้นที่ frame ในสุด จากนั้น Python จะ unwind ทีละ frame เช็คแต่ละ try ที่ครอบอยู่หา except ที่ตรงกัน จนกว่าจะเจอหรือ stack หมด (ไม่มีใครจับ → interpreter พิมพ์ traceback แล้ว exit) finally block ตลอดทางจะรันเสมอระหว่าง unwind นี้ ไม่ว่าจะเป็นตัวที่จับ อะไรได้หรือไม่ก็ตาม การอ่าน traceback ก็คือการอ่าน log ของการ unwind นี้เอง — ซึ่งเป็นเหตุผลว่าทำไมบรรทัดล่างสุด (จุดที่มันเริ่ม) ถึงมีประโยชน์กว่าบรรทัดบนสุด (โปรแกรมมาถึงจุดนั้นได้อย่างไร) ในฐานะสิ่งแรกที่ควรอ่าน

สิ่งที่ได้ประโยชน์ต้นทุน
except clause แบบเจาะจงจัดการเฉพาะ failure ที่ตั้งใจไว้ bug อื่นยัง crash ให้เห็นชัดต้องเขียนหลาย clause กว่า except กว้าง ๆ ตัวเดียว
except Exceptionhandler เดียวสำหรับ “application error ใดก็ตาม”ยังพลาดสัญญาณควบคุม process โดยตั้งใจ อาจซ่อน bug ที่ควรแก้
EAFP (try/except)ไม่มีช่องโหว่ TOCTOU มักเร็วกว่าบน path ปกติexception handling มี overhead บน path ที่ล้มเหลว
except* / exception groupsการ raise ครั้งเดียวพก failure ที่ไม่เกี่ยวข้องกันหลายตัวได้syntax และ mental model ใหม่ except ธรรมดาจับ group ไม่ได้
assert สำหรับ validationไม่ต้องเขียนโค้ดเพิ่มเลยหายไปหมดภายใต้ -O — ไม่ validate อะไรเลยใน production ถ้าใช้ flag นั้น
  • “except Exception จับทุกอย่าง” — มันตั้งใจไม่จับ KeyboardInterrupt, SystemExit หรือ GeneratorExit — สิ่งเหล่านั้นสืบทอดจาก BaseException โดยตรง มีแค่ bare except: หรือ except BaseException: เท่านั้นที่จับ พวกนั้นด้วย
  • “raise กับ raise e ใน except block คือสิ่งเดียวกัน” — raise e เพิ่ม traceback frame ใหม่ที่บรรทัด raise e เอง ส่วน bare raise re-raise exception เดิมโดยคง traceback เดิมไว้ไม่แตะต้อง
  • “assert คือวิธีเบา ๆ ในการ validate user input” — มันหายไปทั้งหมดภายใต้ python -O — อย่าใช้กับอะไรก็ตาม ที่ต้องถูกเช็คเสมอ
  • “sys.exit() ฆ่า process ทันที” — มัน raise SystemExit ซึ่งเป็น exception ธรรมดา (แม้จะแปลกไปสักหน่อย) ที่รัน finally block และถูกจับได้ เหมือน exception อื่น ๆ
  • “ควรอ่าน traceback จากบนลงล่าง เหมือนอ่านโค้ด” — บรรทัดล่างสุดคือ error จริง อ่านขึ้นไปจากตรงนั้นเพื่อไล่ว่า execution มาถึงจุดนั้นได้อย่างไร

ก่อนเชื่อ exception handling ที่ AI generate ให้ตรวจสิ่งเหล่านี้:

  • มี bare except: (หรือ except BaseException:) ที่ไม่ re-raise เลยหรือเปล่า? มันจะกลืน KeyboardInterrupt กับ SystemExit ไปด้วย รวมถึง typo ใด ๆ ที่ raise exception ที่ไม่เกี่ยวข้อง
  • มี except: pass (หรือ except Exception: pass) ใช้ “จัดการ” error โดยไม่ log อะไรเลยหรือเปล่า? นั่นคือ bug ที่หายไปแบบเงียบ ๆ ไม่ใช่การจัดการ error
  • โค้ดใช้ assert validate external input (user input, API response, file content) แทนที่จะเป็น internal invariant หรือเปล่า? มันหายไปภายใต้ -O
  • มี raise e ปรากฏใน except ... as e: block ที่ตั้งใจใช้ bare raise หรือเปล่า? เช็คว่า frame ที่เพิ่มมามีผล จริง ๆ ที่นี่หรือไม่ — ปกติแล้วไม่มีผล และ bare raise สะอาดกว่า
  • โค้ด print(e) แทนที่จะเป็น logging.exception(...) ภายใน except block ในอะไรก็ตามที่มากกว่า script เร็ว ๆ หรือเปล่า? traceback หายไปทันทีที่ต้องใช้มัน debug

🐛 หา bug ในโค้ดนี้

def process_order(quantity: int) -> int:
assert quantity > 0, "quantity must be positive"
return quantity * 10
print(process_order(-5))
เฉลย

โค้ดนี้ดูเหมือน input validation และเมื่อรันปกติมันก็ raise AssertionError: quantity must be positive จริง — จึงเชื่อถือได้ง่าย แต่ assert ไม่ใช่ validation: รันไฟล์เดียวกันเป๊ะด้วย python3.14 -O process_order.py แล้ว assertion จะถูกตัดออกจาก bytecode ทั้งหมด process_order(-5) จะคืน -50 แบบเงียบ ๆ และปริมาณ order ที่ติดลบจะไหลเข้าสู่ส่วนที่เหลือของโปรแกรมตรง ๆ ทางแก้คือ if quantity <= 0: raise ValueError("quantity must be positive") ซึ่งไม่มี interpreter flag ไหนเอาออกได้

except clause แบบไหนใน Python 3.14 ที่ถูกต้องโดยไม่ต้องมีวงเล็บ?
Block else ใน try statement ทำงานเมื่อไหร่?
raise RuntimeError('msg') from original ทำอะไร?
เทียบกับการเรียก reraise_bare() จำนวน traceback frame จะเปลี่ยนไปอย่างไรถ้าจับจาก a() แล้วเรียก reraise_e(caught) แทน?
def a():
    b()
def b():
    raise ValueError("x")

def reraise_bare():
    try:
        a()
    except ValueError:
        raise

def reraise_e(err):
    raise err
โค้ดนี้พิมพ์อะไร และทำไมถึงอันตราย?
def risky_lookup(data, key):
    try:
        return data[key]
    except:
        pass
    return -1

print(risky_lookup(None, "x"))
function หนึ่ง validate argument ด้วย assert value > 0, "..." จะเกิดอะไรขึ้นเมื่อรันโปรแกรมด้วย python -O?
ทำไม except Exception: ถึงจับการเรียก sys.exit(1) ไม่ได้?