- Hiểu cơ chế exception: stack unwinding, propagate, ai bắt thì dừng.
- Dùng đủ bộ tứ
try/except/else/finally— biết vai trò riêng của từng khối. - Đọc được cây phân cấp exception built-in; bắt đúng độ rộng, không bắt thừa.
- Chủ động
raise; giữ nguyên nhân gốc bằngraise ... from ...(exception chaining). - Thiết kế custom exception thành một cây lỗi riêng cho ứng dụng/thư viện.
- Phân biệt EAFP và LBYL; biết khi nào chọn phong cách nào.
- Đọc traceback từ dưới lên, hiểu "During handling..." và "The above exception was the direct cause...".
- Tránh 5 anti-pattern kinh điển: bare
except, nuốt lỗi, bắt quá rộng, dùng exception thayif, log rồiraiselại nhiều lần. - Biết
ExceptionGroupvàexcept*(Python 3.11+),assert,warnings, và cách log lỗi chuẩn.
Unless explicitly silenced.
Hai dòng trên là nguyên tắc số 10 và 11 của Zen of Python, và chúng chính là tinh thần của cả chương này.
"Lỗi không bao giờ được đi qua trong im lặng" — nghĩa là except: pass theo thói quen là phản
Pythonic. "Trừ khi được làm im lặng một cách tường minh" — nghĩa là vẫn có lúc bạn cố tình bỏ qua
một loại lỗi, nhưng phải nói rõ loại nào và vì sao.
1. Exception là gì — cơ chế bên dưới
Khi Python gặp tình huống không thể tiếp tục theo cách bình thường, nó tạo một object exception
rồi "ném" object đó lên. Từ điểm ném, Python bắt đầu gỡ ngăn xếp (stack unwinding):
thoát khỏi hàm hiện tại, quay về hàm gọi nó, rồi hàm gọi nữa… cho tới khi tìm được một khối
except phù hợp. Nếu đi hết ngăn xếp mà không ai bắt, interpreter in traceback và kết thúc chương
trình với exit code khác 0.
def level3():
print(" level3: bắt đầu")
raise ValueError("hỏng ở đáy")
print(" level3: dòng này KHÔNG BAO GIỜ chạy")
def level2():
print(" level2: gọi level3")
level3()
print(" level2: dòng này cũng bị bỏ qua")
def level1():
try:
level2()
except ValueError as e:
print(f"level1: bắt được -> {e}")
level1()
# level2: gọi level3
# level3: bắt đầu
# level1: bắt được -> hỏng ở đáy
Hãy để ý hai dòng print sau chỗ ném — chúng không chạy. Exception cắt ngang luồng
thực thi ngay tại điểm ném, không có chuyện "chạy nốt rồi mới báo lỗi". Đây là khác biệt căn bản với việc
trả về mã lỗi (error code) kiểu C: mã lỗi có thể bị bỏ qua, exception thì không.
Hình dung ngăn xếp gọi hàm là một cầu thang, main ở tầng trệt, hàm sâu nhất ở tầng cao nhất.
raise thả một quả bóng từ tầng cao. Bóng rơi xuống từng tầng; ở mỗi tầng, nếu có một
except "cỡ vừa tay" thì tầng đó bắt lấy và trò chơi dừng. Nếu không tầng nào bắt, bóng rơi
xuống đất — interpreter in traceback: danh sách các tầng mà quả bóng đã đi qua, ghi từ tầng trệt
lên tầng cao nhất.
JS: try { } catch (e) { } finally { } — chỉ có một khối catch
duy nhất, phải tự if (e instanceof TypeError) bên trong để phân loại. Không có khối
else.
Dart: try { } on FormatException catch (e) { } catch (e, st) { } finally { }
— có on Type để lọc theo kiểu, gần với Python nhất; st là stack trace tách riêng.
Python: nhiều khối except LoaiLoi nối tiếp, cộng thêm khối else
mà cả JS lẫn Dart đều không có. Stack trace nằm sẵn trong e.__traceback__.
2. try / except — cú pháp nền
Cấu trúc tối thiểu gồm khối try (đoạn code có thể hỏng) và ít nhất một khối except
(cách xử lý khi hỏng). Python so khớp exception vừa ném với từng except theo thứ tự từ
trên xuống, và chỉ khối đầu tiên khớp được chạy.
def chia(a: float, b: float) -> float | None:
try:
return a / b
except ZeroDivisionError:
print("Không chia được cho 0")
return None
print(chia(10, 2)) # 5.0
print(chia(10, 0)) # Không chia được cho 0 → None
2.1. as e — cầm lấy object exception
Mệnh đề as e gán object exception vào biến e để bạn đọc thông tin bên trong.
Object này không chỉ chứa thông điệp: nó có args (tuple tham số khi khởi tạo),
__traceback__ (vết ngăn xếp), và với nhiều loại lỗi còn có field chuyên biệt
(OSError.errno, OSError.filename, UnicodeDecodeError.start…).
try:
open("/khong/ton/tai.txt")
except OSError as e:
print(type(e).__name__) # FileNotFoundError
print(e.args) # (2, 'No such file or directory')
print(e.errno) # 2
print(e.filename) # /khong/ton/tai.txt
print(str(e)) # [Errno 2] No such file or directory: '/khong/ton/tai.txt'
e bị xóa sau khối except
Python tự động del e khi ra khỏi khối except (để tránh giữ
tham chiếu vòng qua traceback gây rò rỉ bộ nhớ). Vì vậy đoạn dưới ném NameError:
try:
1 / 0
except ZeroDivisionError as e:
pass
print(e) # NameError: name 'e' is not defined
# Muốn dùng ngoài khối → gán sang biến khác BÊN TRONG khối
loi = None
try:
1 / 0
except ZeroDivisionError as e:
loi = e # ✅ giữ lại tham chiếu
print(loi) # division by zero
2.2. Nhiều khối except và bắt gộp bằng tuple
Có hai cách xử lý nhiều loại lỗi: viết nhiều khối except nối tiếp (khi mỗi loại cần xử lý
khác nhau), hoặc gộp vào một tuple (khi cách xử lý giống nhau).
def doc_tuoi(du_lieu: dict) -> int:
try:
return int(du_lieu["tuoi"])
except KeyError: # thiếu khóa → mặc định
return 0
except (ValueError, TypeError): # gộp: cùng cách xử lý
raise ValueError(f"tuoi không hợp lệ: {du_lieu['tuoi']!r}")
print(doc_tuoi({"tuoi": "22"})) # 22
print(doc_tuoi({})) # 0
print(doc_tuoi({"tuoi": "abc"})) # ValueError: tuoi không hợp lệ: 'abc'
Vì Python chọn khối except đầu tiên khớp, và một exception con luôn khớp với khối
bắt lớp cha, nên đặt lớp cha lên trước sẽ khiến khối con không bao giờ chạy:
# ❌ SAI — FileNotFoundError là con của OSError
try:
open("x.txt")
except OSError:
print("lỗi hệ thống") # luôn chạy khối này
except FileNotFoundError:
print("không thấy file") # CHẾT — không bao giờ tới
# ✅ ĐÚNG — cụ thể trước, tổng quát sau
try:
open("x.txt")
except FileNotFoundError:
print("không thấy file")
except OSError:
print("lỗi hệ thống khác")
ruff bắt lỗi này bằng rule B014/E722 nhóm; nhưng đừng phụ thuộc
linter — hãy nhớ quy tắc "cụ thể trước, tổng quát sau" như một phản xạ.
3. Cây phân cấp exception built-in
Mọi thứ ném được trong Python đều kế thừa từ BaseException. Nhánh quan trọng nhất với ứng dụng
là Exception. Ba lớp nằm ngoài Exception — SystemExit,
KeyboardInterrupt, GeneratorExit — được cố tình tách ra vì chúng không phải "lỗi
nghiệp vụ" mà là tín hiệu điều khiển: bạn gần như không bao giờ muốn bắt nhầm chúng.
BaseException
├── SystemExit # sys.exit() — để chương trình thoát bình thường
├── KeyboardInterrupt # Ctrl+C — để người dùng dừng được
├── GeneratorExit # generator.close()
└── Exception # ⭐ mọi lỗi "nghiệp vụ" nằm dưới đây
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError # lst[99] khi lst chỉ có 3 phần tử
│ └── KeyError # d["thieu"]
├── OSError # alias: IOError, EnvironmentError
│ ├── FileNotFoundError
│ ├── PermissionError
│ ├── FileExistsError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── TimeoutError
│ └── ConnectionError
│ ├── ConnectionRefusedError
│ ├── ConnectionResetError
│ └── BrokenPipeError
├── ValueError # đúng kiểu, sai giá trị: int("abc")
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ └── UnicodeEncodeError
├── TypeError # sai kiểu: len(5)
├── AttributeError # obj.khong_co
├── NameError # dùng biến chưa gán
│ └── UnboundLocalError
├── ImportError
│ └── ModuleNotFoundError
├── RuntimeError
│ ├── RecursionError
│ └── NotImplementedError
├── StopIteration # next() hết phần tử — for tự bắt
├── StopAsyncIteration
├── AssertionError # assert thất bại
└── Warning # DeprecationWarning, UserWarning...
| Exception | Nghĩa | Ví dụ gây ra |
|---|---|---|
ValueError | Kiểu đúng nhưng giá trị không dùng được | int("abc"), math.sqrt(-1) |
TypeError | Sai kiểu dữ liệu hoặc sai chữ ký hàm | "a" + 1, len(5) |
KeyError | Khóa không tồn tại trong mapping | {"a": 1}["b"] |
IndexError | Index vượt phạm vi sequence | [1, 2][9] |
AttributeError | Object không có attribute đó | "abc".push("d") |
FileNotFoundError | File/thư mục không tồn tại | open("thieu.txt") |
PermissionError | Không đủ quyền truy cập | open("/etc/shadow") |
ZeroDivisionError | Chia (hoặc modulo) cho 0 | 1 / 0, 5 % 0 |
StopIteration | Iterator đã hết phần tử | next(iter([])) |
RecursionError | Vượt giới hạn đệ quy (mặc định ~1000) | hàm tự gọi vô hạn |
ModuleNotFoundError | Không tìm thấy module khi import | import khong_ton_tai |
NotImplementedError | Method trừu tượng chưa được cài đặt | base class cố tình raise |
Câu hỏi tự đặt: "Người gọi sai kiểu, hay sai giá trị?" Sai kiểu (truyền list vào chỗ
cần str) → TypeError. Đúng kiểu nhưng giá trị vô nghĩa (tuổi âm, email không có
@) → ValueError. Trạng thái object không cho phép thao tác này (rút tiền khi tài
khoản đã đóng) → RuntimeError hoặc custom exception của bạn. Đừng ném
Exception trần — nó buộc người gọi phải bắt quá rộng.
4. Bộ tứ đầy đủ — try / except / else / finally
Python có bốn khối, nhiều hơn hầu hết ngôn ngữ. Rất nhiều người viết Python nhiều năm vẫn
không dùng else — trong khi nó chính là công cụ để giữ khối try hẹp nhất có
thể, điều kiện tiên quyết để không bắt nhầm lỗi.
| Khối | Chạy khi nào | Dùng để làm gì |
|---|---|---|
try | Luôn chạy đầu tiên | Chỉ đặt đúng thao tác có thể ném — càng ngắn càng tốt |
except | Khi có exception khớp | Xử lý / chuyển đổi / ghi log lỗi |
else | Khi try không ném gì | Phần "đường sáng" — code chỉ đúng khi thao tác rủi ro đã thành công |
finally | Luôn luôn, kể cả có return/break | Dọn dẹp tài nguyên: đóng file, nhả lock, rollback |
import json
def doc_config(duong_dan: str) -> dict:
f = None
try:
f = open(duong_dan, encoding="utf-8") # ⚠️ chỉ thao tác rủi ro
except FileNotFoundError:
print(f"Không thấy {duong_dan}, dùng config rỗng")
return {}
else:
# chỉ tới đây khi open() THÀNH CÔNG
return json.load(f)
finally:
# chạy dù thành công, dù lỗi, dù đã return ở trên
if f is not None:
f.close()
print("đã đóng file")
Vì sao else đáng dùng?
Nếu bỏ else và nhét json.load(f) vào trong try, thì một
FileNotFoundError tưởng tượng ném ra từ bên trong json.load (ví dụ
hook custom của bạn mở thêm file khác) cũng sẽ bị khối except FileNotFoundError nuốt mất và
trả về {} — sai hoàn toàn. else giữ cho try chỉ chứa
đúng một thao tác mà bạn thực sự muốn bảo vệ.
4.1. finally và cái bẫy return
finally chạy kể cả khi khối trước đó đã return. Nếu chính
finally cũng có return, giá trị của nó ghi đè giá trị trước — và
tệ hơn, nó nuốt luôn exception đang bay. Đây là một trong những bug khó tìm nhất Python.
def xau():
try:
return "từ try"
finally:
return "từ finally" # ❌ ghi đè
print(xau()) # 'từ finally'
def rat_xau():
try:
raise ValueError("lỗi nghiêm trọng!")
finally:
return "ổn cả" # ❌❌ NUỐT exception, không ai biết có lỗi
print(rat_xau()) # 'ổn cả' — ValueError biến mất hoàn toàn
Không bao giờ đặt return, break, hay continue
trong khối finally. Chỉ đặt code dọn dẹp. Nếu cần chọn giá trị trả về theo kết quả, dùng
else. ruff có rule B012 để chặn chuyện này.
with thay được phần lớn finally
Ví dụ doc_config ở trên viết bằng with gọn hơn nhiều — with tự gọi
close() dù có lỗi hay không. Bạn sẽ dùng with liên tục ở
Chương 7 (File I/O), và học cách tự viết context manager ở
Chương 10. finally vẫn cần khi tài nguyên không có sẵn context
manager, hoặc khi cần dọn dẹp thứ không phải "tài nguyên" (khôi phục biến toàn cục, ghi metric…).
5. raise — chủ động ném lỗi
Viết thư viện hay viết hàm nghiệp vụ, bạn sẽ ném lỗi nhiều hơn là bắt lỗi. Nguyên tắc: ném sớm, ném rõ (fail fast). Một hàm nhận dữ liệu sai mà vẫn cố chạy tiếp sẽ đẩy lỗi đi xa khỏi nguyên nhân, khiến việc debug về sau cực khổ.
def dat_tuoi(tuoi: int) -> int:
if not isinstance(tuoi, int):
raise TypeError(f"tuoi phải là int, nhận {type(tuoi).__name__}")
if not 0 <= tuoi <= 150:
raise ValueError(f"tuoi phải trong [0, 150], nhận {tuoi}")
return tuoi
dat_tuoi("22") # TypeError: tuoi phải là int, nhận str
dat_tuoi(-5) # ValueError: tuoi phải trong [0, 150], nhận -5
- Cái gì sai — "tuoi phải trong [0, 150]"
- Nhận được gì — "nhận -5" (dùng
!rtrong f-string để thấy rõ chuỗi:f"{x!r}"cho'abc'thay vìabc) - Sửa thế nào — khi không hiển nhiên: "dùng
--forceđể ghi đè"
Thông điệp tệ: raise ValueError("invalid input"). Người đọc traceback lúc 2 giờ sáng sẽ
không biết input nào, sai chỗ nào.
5.1. raise trần — ném lại nguyên vẹn
Trong khối except, viết raise không kèm gì sẽ ném lại chính exception
đang xử lý, giữ nguyên traceback gốc. Đây là cách chuẩn để "ghi log rồi để lỗi đi tiếp".
import logging
def xu_ly_don(don_id: int) -> None:
try:
luu_vao_db(don_id)
except Exception:
logging.exception(f"Lưu đơn {don_id} thất bại")
raise # ✅ ném lại y nguyên, traceback gốc còn đủ
# ❌ SAI — mất traceback gốc, chỉ thấy dòng raise này
def xu_ly_don_sai(don_id: int) -> None:
try:
luu_vao_db(don_id)
except Exception as e:
raise Exception(str(e)) # mất kiểu, mất traceback, mất context
6. Exception chaining — raise ... from ...
Khi bạn bắt một lỗi cấp thấp và muốn ném ra một lỗi cấp cao dễ hiểu hơn cho người dùng thư viện, hãy
nối chuỗi chúng bằng from. Python sẽ in cả hai trong traceback, giữ nguyên
manh mối gốc.
class ConfigError(Exception):
"""Lỗi khi nạp file cấu hình."""
def nap_config(duong_dan: str) -> dict:
try:
with open(duong_dan, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError as e:
raise ConfigError(f"Thiếu file cấu hình: {duong_dan}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"Cấu hình {duong_dan} sai cú pháp JSON") from e
Traceback in ra sẽ có hai tầng, nối bằng một câu giải thích:
Traceback (most recent call last):
File "app.py", line 8, in nap_config
with open(duong_dan, encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'settings.json'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 20, in <module>
cfg = nap_config("settings.json")
ConfigError: Thiếu file cấu hình: settings.json
| Cách viết | Attribute được set | Câu nối trong traceback | Khi nào dùng |
|---|---|---|---|
raise B from a |
B.__cause__ = a |
"…was the direct cause of…" | Bạn chủ ý chuyển đổi lỗi — trường hợp phổ biến nhất |
raise B (trong except) |
B.__context__ = a (tự động) |
"During handling of the above exception, another exception occurred" | Ngoài ý muốn — thường là dấu hiệu handler của bạn cũng bị lỗi |
raise B from None |
__suppress_context__ = True |
Không in lỗi gốc | Lỗi gốc là chi tiết nội bộ, gây nhiễu cho người dùng thư viện |
Nếu traceback của bạn hiện câu "During handling of the above exception, another exception occurred"
mà bạn không cố ý, hãy đọc kỹ: nó có nghĩa là khối except của bạn cũng ném lỗi.
Ví dụ kinh điển: trong except KeyError: bạn viết print(f"thiếu {du_lieu['key']}")
— và lại gây KeyError lần nữa. Handler phải là đoạn code an toàn nhất trong hàm.
7. Custom exception — thiết kế cây lỗi cho ứng dụng
Với bất kỳ thư viện hoặc ứng dụng nào lớn hơn một script, hãy tạo một exception gốc riêng
rồi cho mọi lỗi của bạn kế thừa từ nó. Lợi ích: người dùng thư viện có thể viết
except TenAppError: để bắt tất cả lỗi của bạn mà không vô tình bắt lỗi của thư viện khác.
class ThanhToanError(Exception):
"""Lớp gốc cho mọi lỗi của module thanh toán."""
class TheKhongHopLe(ThanhToanError):
"""Số thẻ sai định dạng hoặc đã hết hạn."""
class KhongDuSoDu(ThanhToanError):
"""Tài khoản không đủ tiền để thực hiện giao dịch."""
def __init__(self, can: int, co: int):
self.can = can
self.co = co
self.thieu = can - co
super().__init__(f"Cần {can:,}đ nhưng chỉ có {co:,}đ (thiếu {self.thieu:,}đ)")
class CongThanhToanLoi(ThanhToanError):
"""Cổng thanh toán bên thứ ba không phản hồi hoặc trả lỗi."""
def rut_tien(so_du: int, so_tien: int) -> int:
if so_tien > so_du:
raise KhongDuSoDu(can=so_tien, co=so_du)
return so_du - so_tien
# Người gọi có thể bắt ở nhiều mức chi tiết khác nhau
try:
rut_tien(50_000, 200_000)
except KhongDuSoDu as e:
print(f"Nạp thêm {e.thieu:,}đ để tiếp tục") # Nạp thêm 150,000đ để tiếp tục
except ThanhToanError:
print("Giao dịch thất bại, vui lòng thử lại") # lưới an toàn cho cả module
Đặt dữ liệu vào exception, đừng chỉ đặt chữ
KhongDuSoDu mang theo can, co, thieu dưới dạng
attribute — nhờ vậy handler có thể hành động (hiện nút "Nạp thêm 150.000đ") thay vì chỉ hiển thị
chuỗi. Nếu chỉ có message, handler buộc phải parse chuỗi — cách làm dễ vỡ nhất.
PEP 8 khuyến nghị tên exception kết thúc bằng Error nếu đó là lỗi
(ConfigError, ValidationError). Nếu là tín hiệu điều khiển chứ không phải lỗi
(hiếm), có thể bỏ hậu tố — như chính StopIteration của Python. Trong ví dụ trên tôi dùng tên
tiếng Việt để dễ đọc, nhưng ở dự án thật hãy giữ tên class tiếng Anh cho nhất quán với hệ sinh thái.
8. EAFP vs LBYL — hai trường phái
Đây là một trong những điểm khiến code Python trông khác code Java/Go rõ rệt.
| LBYL — Look Before You Leap | EAFP — Easier to Ask Forgiveness than Permission | |
|---|---|---|
| Tư tưởng | Kiểm tra điều kiện trước khi làm | Cứ làm, hỏng thì bắt |
| Cú pháp | if / hasattr / in | try / except |
| Ưu điểm | Rõ ràng khi điều kiện đơn giản | Không có khoảng trống race condition; ít lặp việc |
| Nhược điểm | Dính race condition; kiểm tra 2 lần (check + thao tác) | Khó đọc nếu khối try quá dài |
| Python thiên về | Ít hơn | Mặc định — idiom chuẩn |
import os
# LBYL — có lỗ hổng race condition
if os.path.exists("data.csv"):
# ⚠️ giữa dòng trên và dòng dưới, tiến trình khác có thể xoá file!
with open("data.csv") as f:
noi_dung = f.read()
else:
noi_dung = ""
# EAFP — không có khoảng trống, đúng cả khi có nhiều tiến trình
try:
with open("data.csv") as f:
noi_dung = f.read()
except FileNotFoundError:
noi_dung = ""
Race condition trong ví dụ LBYL trên có tên riêng: TOCTOU (Time-Of-Check to Time-Of-Use). Nó là nguồn của nhiều lỗ hổng bảo mật thật, không chỉ là lý thuyết. Với thao tác trên file, socket, hay bất kỳ tài nguyên chia sẻ nào, EAFP là lựa chọn đúng, không chỉ là lựa chọn "Pythonic".
- Điều kiện thuần tuý tính toán, không đụng tài nguyên ngoài:
if n > 0:rõ hơntry: math.sqrt(n). - Có sẵn API "an toàn":
d.get(k, default)đẹp hơntry: d[k] except KeyErrorcho trường hợp một khóa. - Exception xảy ra thường xuyên (không phải ngoại lệ): ném/bắt exception tốn hơn một phép
ifnhiều lần. Nếu 90% trường hợp là "thiếu khóa" thìifnhanh hơn.
9. Đọc traceback — kỹ năng bắt buộc
Traceback là bản đồ dẫn tới chỗ hỏng. Quy tắc số một: đọc từ dưới lên. Dòng cuối cùng là loại lỗi và thông điệp; ngay phía trên là dòng code thực sự gây lỗi; càng lên cao là các hàm gọi xa hơn.
Traceback (most recent call last):
File "/app/main.py", line 42, in <module> # ④ điểm khởi đầu
bao_cao = tong_hop(don_hangs)
^^^^^^^^^^^^^^^^^^^
File "/app/report.py", line 15, in tong_hop # ③
return sum(tinh_gia(d) for d in don_hangs)
^^^^^^^^^^^
File "/app/report.py", line 8, in tinh_gia # ② hàm gọi trực tiếp
return don["so_luong"] * don["don_gia"]
~~~^^^^^^^^^^^
KeyError: 'don_gia' # ① ĐỌC DÒNG NÀY TRƯỚC
| Thứ tự đọc | Nội dung | Câu hỏi cần trả lời |
|---|---|---|
| ① Dòng cuối | KeyError: 'don_gia' | Lỗi loại gì? Về cái gì? |
| ② Frame cuối | report.py:8, in tinh_gia | Dòng code nào thực sự nổ? |
| ③④ Các frame trên | chuỗi hàm gọi | Dữ liệu xấu đến từ đâu? |
Từ Python 3.11, traceback có thêm chỉ dẫn ^^^ và ~~~ chỉ đúng
biểu thức con gây lỗi. Trong ví dụ trên, ~~~^^^ cho biết lỗi ở don["don_gia"] chứ
không phải don["so_luong"] — trước 3.11 bạn phải tự đoán giữa hai phép truy cập trên cùng một dòng.
import traceback
try:
1 / 0
except ZeroDivisionError as e:
# In ra stderr như Python vẫn làm
traceback.print_exc()
# Lấy về dưới dạng chuỗi — để gửi lên hệ thống giám sát
chuoi = traceback.format_exc()
# Duyệt từng frame dưới dạng object
for frame in traceback.extract_tb(e.__traceback__):
print(f"{frame.filename}:{frame.lineno} trong {frame.name}()")
10. Năm anti-pattern kinh điển
# ❌ 1. BARE EXCEPT — bắt cả Ctrl+C và sys.exit()
try:
tinh_toan_lau()
except: # bắt cả BaseException!
print("có lỗi") # người dùng bấm Ctrl+C mà không thoát được
# ✅ Sửa: nêu rõ loại, hoặc ít nhất dùng Exception
try:
tinh_toan_lau()
except Exception: # KeyboardInterrupt/SystemExit đi qua bình thường
logging.exception("tính toán thất bại")
raise
# ❌ 2. NUỐT LỖI — pass trong except
try:
cfg = nap_config()
except Exception:
pass # cfg giờ không tồn tại → NameError ở dưới
# ✅ Sửa: hoặc xử lý, hoặc để nó bay lên. Muốn im lặng thì nói rõ loại:
from contextlib import suppress
with suppress(FileNotFoundError): # "explicitly silenced"
os.remove("tam.tmp")
# ❌ 3. TRY QUÁ RỘNG — không biết dòng nào hỏng
try:
du_lieu = tai_ve(url)
ket_qua = phan_tich(du_lieu)
luu(ket_qua)
gui_email(ket_qua)
except Exception:
print("thất bại") # thất bại ở bước nào? không ai biết
# ✅ Sửa: bọc từng bước rủi ro, thông điệp riêng cho từng bước
# ❌ 4. DÙNG EXCEPTION THAY IF (khi if hoàn toàn đủ)
try:
x = so_am
except:
x = 0
# ✅ Sửa: điều kiện thuần tính toán thì dùng if
x = so_am if so_am >= 0 else 0
# ❌ 5. LOG RỒI RAISE Ở MỌI TẦNG — traceback lặp 5 lần trong log
def tang_giua():
try:
tang_duoi()
except Exception:
logging.exception("lỗi") # tầng nào cũng log → log ngập rác
raise
# ✅ Sửa: chỉ log ở BIÊN — nơi lỗi dừng lại (handler HTTP, main, worker loop).
# Các tầng giữa: hoặc im lặng để nó bay, hoặc chaining thêm ngữ cảnh.
except Exception — hợp lệ nhưng chỉ ở đúng chỗ
Bắt Exception là đúng ở đúng một loại vị trí: biên của chương trình
— vòng lặp worker không được chết vì một job hỏng, handler HTTP phải trả 500 thay vì sập server, hàm
main() muốn in thông điệp thân thiện thay vì traceback thô. Ở mọi nơi khác trong code nghiệp
vụ, hãy nêu tên loại lỗi cụ thể.
11. ExceptionGroup và except* (Python 3.11+)
Trước 3.11, một khối try chỉ ném được một exception. Điều đó thành vấn đề khi bạn chạy
nhiều tác vụ song song: nếu 3 trong 10 request thất bại, bạn chỉ thấy được lỗi đầu tiên. Python 3.11 giới
thiệu ExceptionGroup — một exception chứa nhiều exception con — cùng cú pháp
except* để bắt theo loại xuyên qua nhóm.
def kiem_tra_tat_ca(nguoi_dung: dict) -> None:
"""Gom TẤT CẢ lỗi validate thay vì dừng ở lỗi đầu tiên."""
loi: list[Exception] = []
if not nguoi_dung.get("email", "").count("@"):
loi.append(ValueError("email không hợp lệ"))
if nguoi_dung.get("tuoi", 0) < 18:
loi.append(ValueError("phải từ 18 tuổi"))
if not nguoi_dung.get("mat_khau"):
loi.append(KeyError("mat_khau"))
if loi:
raise ExceptionGroup("Dữ liệu đăng ký không hợp lệ", loi)
try:
kiem_tra_tat_ca({"email": "sai", "tuoi": 15})
except* ValueError as nhom:
for e in nhom.exceptions:
print(f" giá trị sai: {e}")
except* KeyError as nhom:
for e in nhom.exceptions:
print(f" thiếu trường: {e}")
# giá trị sai: email không hợp lệ
# giá trị sai: phải từ 18 tuổi
# thiếu trường: 'mat_khau'
except* khác except ở ba điểm- Nhiều khối cùng chạy: không dừng ở khối đầu tiên khớp — cả
except* ValueErrorlẫnexcept* KeyErrorđều chạy. - Biến bắt được luôn là một nhóm, không phải một exception đơn — phải duyệt
nhom.exceptions. - Không trộn được: trong cùng một
try, hoặc dùng toànexcept, hoặc toànexcept*.
Nơi bạn gặp ExceptionGroup nhiều nhất là asyncio.TaskGroup —
Chương 11 (Async Python) sẽ dùng lại chính xác cơ chế này.
12. assert, warnings, và cách ghi log lỗi
12.1. assert — chỉ để bắt lỗi lập trình
assert dieu_kien, thong_diep ném AssertionError khi điều kiện sai. Điểm mấu chốt:
Python xoá sạch mọi assert khi chạy với cờ tối ưu python -O.
Vì vậy tuyệt đối không dùng assert để kiểm tra dữ liệu người dùng hay điều kiện bảo mật.
# ✅ ĐÚNG — kiểm tra bất biến nội bộ ("chuyện này không thể xảy ra")
def chia_deu(tong: int, phan: list[int]) -> None:
# ... thuật toán chia ...
assert sum(phan) == tong, f"bug thuật toán: {sum(phan)} != {tong}"
# ❌ SAI — kiểm tra input người dùng bằng assert
def dang_nhap(mat_khau: str) -> None:
assert len(mat_khau) >= 8 # biến mất khi chạy python -O !!
# ✅ Sửa
def dang_nhap(mat_khau: str) -> None:
if len(mat_khau) < 8:
raise ValueError("mật khẩu phải từ 8 ký tự")
assert với tuple luôn đúngassert (x > 0, "x phải dương") # ❌ đây là 1 TUPLE — luôn truthy!
assert x > 0, "x phải dương" # ✅ đúng — không có ngoặc
Bản Python mới cảnh báo SyntaxWarning cho trường hợp này, nhưng nó vẫn lọt qua ở nhiều
codebase cũ. ruff rule F631 bắt được.
12.2. warnings — báo mà không dừng
Khi muốn nói "cách dùng này sắp bị bỏ" hoặc "kết quả có thể không như mong đợi" mà không làm hỏng
chương trình, dùng module warnings.
import warnings
def tinh_thue_cu(x: float) -> float:
warnings.warn(
"tinh_thue_cu() sẽ bị xoá ở v3.0, dùng tinh_thue() thay thế",
DeprecationWarning,
stacklevel=2, # trỏ vào NGƯỜI GỌI, không phải dòng này
)
return x * 0.1
# Biến warning thành lỗi trong test — để không ai lỡ dùng API cũ
warnings.simplefilter("error", DeprecationWarning)
12.3. Ghi log lỗi cho đúng
import logging
log = logging.getLogger(__name__)
try:
xu_ly()
except Exception:
# ✅ log.exception() — CHỈ gọi trong khối except, tự đính kèm traceback
log.exception("Xử lý thất bại")
# tương đương:
# log.error("Xử lý thất bại", exc_info=True)
# ❌ mất traceback — chỉ còn dòng chữ, không biết hỏng ở đâu
# log.error(f"Xử lý thất bại: {e}")
- Dùng
log.exception(...)(không phảilog.error(f"...{e}")) — để giữ traceback. - Log ở biên, nơi lỗi dừng lại — không log lại ở mọi tầng.
- Đừng log dữ liệu nhạy cảm (mật khẩu, token, số thẻ) vào thông điệp lỗi — chúng sẽ nằm trong log file mãi mãi.
13. Cheat sheet chương 5
| Tình huống | Cách viết |
|---|---|
| Bắt một loại lỗi | except ValueError as e: |
| Bắt nhiều loại, cùng cách xử lý | except (ValueError, TypeError) as e: |
| Bắt mọi lỗi nghiệp vụ (ở biên) | except Exception: — không phải except: |
| Ném lại nguyên vẹn | raise (trần, trong except) |
| Đổi loại lỗi, giữ nguyên nhân | raise MyError("...") from e |
| Đổi loại lỗi, giấu nguyên nhân | raise MyError("...") from None |
| Bỏ qua một loại lỗi có chủ đích | with suppress(FileNotFoundError): ... |
| Chỉ chạy khi không có lỗi | khối else: |
| Luôn dọn dẹp | khối finally: (không return trong đó!) |
| Gom nhiều lỗi song song | raise ExceptionGroup("...", ds) + except* |
| Ghi log kèm traceback | log.exception("...") |
| Kiểm tra bất biến nội bộ | assert dk, "msg" (không dùng cho input!) |
Bài tập
Hàm chuyen_so_an_toan
Viết chuyen_so_an_toan(gia_tri, mac_dinh=None) nhận bất kỳ thứ gì và trả về
float. Yêu cầu:
- Chuỗi số (
"3.14"," 42 ") → chuyển được, cóstrip(). - Chuỗi rác (
"abc"),None,list→ trảmac_dinh. - Nếu
mac_dinh is None→ thay vì trảNone, hãyraise ValueErrorcó thông điệp nêu rõ giá trị nhận được (dùng!r). - Viết bằng phong cách EAFP, không dùng
isinstancehaystr.isdigit().
Gợi ý
float(x) ném ValueError với chuỗi rác và TypeError với
None/list — bắt gộp cả hai bằng tuple. Dùng sentinel để phân biệt
"người dùng không truyền mac_dinh" với "người dùng truyền None":
_KHONG_TRUYEN = object() làm giá trị mặc định.
Cây lỗi cho một thư viện HTTP mini
Thiết kế cây exception cho một client HTTP giả lập, gồm:
HttpError # gốc — mọi lỗi của thư viện
├── ConnectionFailed # không kết nối được (mang theo .host, .port)
├── Timeout # quá hạn (mang theo .seconds)
└── StatusError # server trả mã lỗi (mang theo .status, .body)
├── ClientError # 4xx
└── ServerError # 5xx
Viết hàm raise_for_status(status: int, body: str): không làm gì nếu 2xx/3xx, ném
ClientError nếu 400-499, ServerError nếu ≥500. Sau đó viết một khối
try minh hoạ ba mức bắt: bắt riêng ServerError (để retry), bắt
StatusError (để log), bắt HttpError (lưới cuối).
Gợi ý
StatusError.__init__(self, status, body) nên gọi
super().__init__(f"HTTP {status}: {body[:100]}") để message tự sinh, đồng thời lưu
self.status, self.body. Trong raise_for_status, chọn class
con bằng một biểu thức: cls = ClientError if status < 500 else ServerError.
Đoán output — thứ tự try/except/else/finally
Không chạy code, viết ra output của cả ba lời gọi. Sau đó chạy để kiểm chứng.
def f(n):
try:
print("A")
if n == 0:
raise ValueError("zero")
if n == 1:
return "B"
except ValueError:
print("C")
return "D"
else:
print("E")
return "F"
finally:
print("G")
print(f(0))
print(f(1))
print(f(2))
Gợi ý
Ba câu hỏi dẫn đường: (1) khi try có return thì else có
chạy không? (2) finally chạy trước hay sau khi giá trị return được xác
định? (3) với n = 2, khối nào chạy — try hết bình thường thì đi đâu?
Decorator @retry có backoff
Viết decorator retry(lan=3, cho=(ConnectionError,), delay=0.5): gọi lại hàm tối đa
lan lần nếu nó ném exception thuộc tuple cho, mỗi lần chờ gấp đôi lần trước
(0.5s → 1s → 2s). Yêu cầu:
- Exception không thuộc
chophải bay lên ngay, không retry. - Hết số lần vẫn hỏng → ném lại exception cuối cùng với traceback nguyên vẹn.
- Gói tất cả lỗi đã gặp vào một
ExceptionGrouplàm__cause__để debug được toàn bộ.
Gợi ý
Khung: for i in range(lan): try: return fn(*a, **k) except cho as e: loi.append(e); time.sleep(delay * 2**i).
Sau vòng lặp: raise loi[-1] from ExceptionGroup("tất cả lần thử đều hỏng", loi).
Cú pháp decorator có tham số và functools.wraps sẽ được đào sâu ở
Chương 10 — ở đây chỉ cần hai lớp hàm lồng nhau.
Sửa 5 anti-pattern trong một file thật
Đoạn code dưới mắc đủ 5 lỗi đã học. Tìm và sửa từng lỗi, giải thích ngắn gọn mỗi chỗ sửa.
import json, logging
def xu_ly_hang_loat(duong_dan):
ket_qua = []
try:
f = open(duong_dan)
du_lieu = json.load(f)
for item in du_lieu:
try:
ket_qua.append(item["gia"] * item["sl"])
except:
pass
f.close()
except Exception as e:
logging.error(f"lỗi: {e}")
raise Exception("xử lý thất bại")
finally:
return ket_qua
Gợi ý
Danh sách kiểm: (1) finally: return nuốt lỗi; (2) bare except;
(3) pass nuốt lỗi im lặng; (4) raise Exception(...) làm mất kiểu và
traceback gốc — cần custom exception + from e; (5) try quá rộng và file
không đóng khi có lỗi giữa chừng — thay bằng with open(...).
Bản sửa nên giữ lại được những item hỏng là item nào, thay vì bỏ qua âm thầm.
Quiz
Vì sao except: (bare) nguy hiểm hơn except Exception:?
Xem đáp án
except: bắt BaseException — tức là bắt cả KeyboardInterrupt
(Ctrl+C), SystemExit (sys.exit()) và GeneratorExit. Hệ quả thực
tế: người dùng bấm Ctrl+C mà chương trình không thoát; script gọi sys.exit(1) nhưng vẫn
chạy tiếp và trả exit code 0 làm CI báo pass sai.
except Exception: để ba tín hiệu đó đi qua bình thường. Nếu thật sự cần bắt mọi thứ
(rất hiếm — ví dụ để ghi log crash cuối cùng), hãy viết except BaseException: tường minh
rồi raise lại.
Khối else của try chạy khi nào, và vì sao nó tồn tại?
Xem đáp án
else chạy khi khối try kết thúc mà không ném exception nào
(và không return/break ra ngoài). Lý do tồn tại: giữ khối try
hẹp nhất có thể. Nếu nhét toàn bộ logic "đường sáng" vào try, một exception cùng
loại vô tình phát sinh từ logic đó sẽ bị handler bắt nhầm và bị hiểu sai nguyên nhân.
else tách rạch ròi: try = thao tác rủi ro, else = việc làm tiếp
khi nó thành công.
raise B from a, raise B trong except, và raise B from None khác nhau thế nào?
Xem đáp án
raise B from a đặt B.__cause__ = a, traceback in
"The above exception was the direct cause of the following exception" — bạn chủ
ý chuyển đổi lỗi.
raise B bên trong except đặt B.__context__ tự động, traceback
in "During handling of the above exception, another exception occurred" — thường là
ngoài ý muốn, dấu hiệu handler của bạn cũng lỗi.
raise B from None bật __suppress_context__, ẩn hoàn toàn lỗi gốc — dùng khi
lỗi gốc là chi tiết cài đặt nội bộ chỉ gây nhiễu cho người dùng thư viện.
Đoạn này in gì? def f(): try: return 1 finally: return 2
Xem đáp án
In 2. finally luôn chạy trước khi hàm thực sự trả về, và một
return trong finally ghi đè giá trị đã chọn ở
try. Tệ hơn: nếu try đang ném exception mà finally có
return, exception bị nuốt hoàn toàn — chương trình chạy tiếp như không
có gì xảy ra. Quy tắc: finally chỉ chứa code dọn dẹp, không bao giờ chứa
return/break/continue.
Vì sao đặt except OSError trước except FileNotFoundError là sai?
Xem đáp án
FileNotFoundError kế thừa OSError. Python duyệt các khối except
từ trên xuống và chạy khối đầu tiên khớp — mà một FileNotFoundError
cũng là một OSError, nên khối trên luôn thắng. Khối
except FileNotFoundError phía dưới trở thành code chết. Quy tắc chung:
cụ thể trước, tổng quát sau. Nếu không chắc quan hệ cha-con, kiểm tra bằng
FileNotFoundError.__mro__ hoặc issubclass(FileNotFoundError, OSError).
Khi nào EAFP thực sự đúng hơn LBYL, chứ không chỉ "Pythonic hơn"?
Xem đáp án
Khi có race condition. Kiểu if os.path.exists(p): open(p) có một khoảng
thời gian giữa lúc kiểm tra và lúc dùng; trong khoảng đó tiến trình khác (hoặc người dùng) có thể xoá
file, đổi quyền, thay symlink. Lỗ hổng này có tên TOCTOU (Time-Of-Check to
Time-Of-Use) và là nguồn của nhiều lỗ hổng bảo mật thật. Với file, socket, database, hay bất kỳ tài
nguyên chia sẻ nào, try/except là cách duy nhất đúng vì thao tác và việc kiểm
tra xảy ra nguyên tử ở tầng hệ điều hành.
Vì sao không được dùng assert để kiểm tra input người dùng?
Xem đáp án
Khi chạy Python với cờ tối ưu python -O (hoặc biến môi trường
PYTHONOPTIMIZE=1), interpreter xoá bỏ hoàn toàn mọi câu assert
khỏi bytecode. Một hệ thống production bật -O sẽ mất sạch các kiểm tra đó — nếu bạn dùng
assert len(mat_khau) >= 8 thì mật khẩu một ký tự sẽ lọt qua. assert chỉ
dành cho bất biến nội bộ — những điều kiện mà nếu sai thì nghĩa là code của bạn
có bug, không phải người dùng nhập sai. Kiểm tra input luôn dùng if ... raise.
ExceptionGroup giải quyết vấn đề gì mà exception thường không giải quyết được?
Xem đáp án
Vấn đề nhiều lỗi độc lập xảy ra cùng lúc. Một khối try truyền thống chỉ
ném được một exception, nên khi chạy 10 tác vụ song song và 3 tác vụ hỏng, bạn chỉ thấy được lỗi đầu
tiên — hai lỗi kia biến mất. ExceptionGroup (Python 3.11+) gói tất cả vào một object,
còn except* cho phép bắt theo loại xuyên qua nhóm: nhiều khối
except* đều có thể chạy trong cùng một lần. Đây chính là cơ chế
asyncio.TaskGroup dùng để báo cáo lỗi của nhiều task song song, và cũng hữu ích cho
validate form (gom hết lỗi rồi trả về một lượt thay vì bắt người dùng sửa từng lỗi một).
Tổng kết
Sau chương 5, bạn đã master:
- Cơ chế: exception cắt ngang luồng, gỡ ngăn xếp cho tới khi có handler khớp; không ai bắt thì in traceback và thoát.
- Bộ tứ:
tryhẹp nhất có thể,exceptcụ thể trước tổng quát sau,elsecho đường sáng,finallychỉ dọn dẹp. - Cây phân cấp:
BaseException→Exception→ các nhánhLookupError/OSError/ValueError/TypeError… - Ném lỗi: thông điệp đủ 3 phần;
raisetrần để ném lại;fromđể nối chuỗi nguyên nhân;from Noneđể giấu chi tiết nội bộ. - Custom exception: một lớp gốc cho cả module, mang dữ liệu chứ không chỉ chữ.
- EAFP là mặc định của Python — và là lựa chọn đúng (không chỉ đẹp) khi có race condition.
- Traceback: đọc từ dưới lên; hiểu hai câu nối "direct cause" và "During handling".
- Anti-pattern: bare
except, nuốt lỗi,tryquá rộng, exception thayif, log ở mọi tầng. - Công cụ phụ:
ExceptionGroup/except*,assert(chỉ cho bất biến nội bộ),warnings,log.exception().
Kết nối
- Chương 6 (Modules & Packages) —
ImportError/ModuleNotFoundErrorvà mẫutry: import nhanh except ImportError: import chamđể chọn thư viện dự phòng. - Chương 7 (File I/O & Regex) —
FileNotFoundError,PermissionError,UnicodeDecodeErrorgặp hằng ngày;withthay chofinally. - Chương 8 (OOP in Python) — exception cũng là class: kế thừa,
__init__,__str__;NotImplementedErrorcho method trừu tượng. - Chương 9 (Iterators & Generators) —
StopIterationlà exception điều khiển luồng củafor;gen.throw()ném lỗi vào trong generator. - Chương 10 (Decorators & Context Managers) —
__exit__nhận bộ ba(exc_type, exc_val, exc_tb)và có thể nuốt lỗi bằng cách trảTrue;contextlib.suppress. - Chương 11 (Async Python) —
asyncio.TaskGroupnémExceptionGroup;asyncio.CancelledErrorkế thừaBaseExceptionnênexcept Exceptionkhông bắt nhầm. - Chương 13 (Testing) —
pytest.raises(ValueError, match="...")để khẳng định lỗi đúng loại và đúng thông điệp. - JavaScript Chương 2 — đối chiếu: JS chỉ có một khối
catch, phải tự phân loại bằnginstanceof; không cóelse. - Dart Chương 3 —
on Type catch (e, st)tách stack trace thành tham số riêng, gần với Python nhất trong ba ngôn ngữ.