Cơ bản

Ngoại lệ (exception) trong Python: try, except, else, finally, raise và ngoại lệ tự định nghĩa

Cập nhật: · Skillaria.Top

Ngoại lệ (exception) trong Python là một đối tượng báo lỗi, làm dừng luồng chạy bình thường của code cho đến khi có khối try/except bắt lấy nó; else chạy khi không có lỗi, còn finally luôn chạy. Có thể tự gây lỗi bằng lệnh raise, ví dụ raise ValueError("số tiền âm"), còn ngoại lệ riêng được tạo bằng cách kế thừa class Exception.

Ngoại lệ (exception) trong Python là một đối tượng báo hiệu lỗi và làm gián đoạn luồng chạy bình thường của chương trình. Nếu không ai bắt nó, Python in traceback và dừng chương trình. Ngoại lệ được bắt bằng cấu trúc try/except, còn được gây ra bằng lệnh raise. Dưới đây là cách hoạt động, các lỗi phổ biến nhất, và cách xử lý lỗi mà không biến nó thành việc che giấu lỗi.

Ngoại lệ trong Python là gì

Khi một thao tác không thể thực hiện — chia cho 0, truy cập một khóa không tồn tại trong dict, chuyển một chữ thành số — Python tạo một đối tượng ngoại lệ và «ném» nó ra. Hàm hiện tại dừng lại, ngoại lệ được chuyển lên theo chuỗi lời gọi, cho đến khi gặp trình xử lý phù hợp. Nếu không có, chương trình sẽ bị dừng.

>>> 10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Nhân tiện trả lời một câu hỏi thường gặp: khi chia cho 0 trong Python, sẽ có ngoại lệ ZeroDivisionError. Điều này áp dụng cho cả /, // và %.

Cách đọc traceback

Traceback là báo cáo cho biết chương trình đã đi đến lỗi như thế nào. Nên đọc từ dưới lên. Dòng cuối là quan trọng nhất: kiểu ngoại lệ và thông báo. Phía trên là dòng code nơi lỗi xảy ra, và cao hơn nữa là nơi đã gọi nó, cho đến điểm bắt đầu chương trình.

Traceback (most recent call last):
  File "chi_tieu.py", line 9, in <module>
    total = parse_amount(row)
  File "chi_tieu.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: 'mười'

Từ Python 3.11, dưới dòng code có các dấu ~~~^^^ (PEP 657), chỉ ra chính xác biểu thức nào trong dòng đã gây lỗi. Điều này đặc biệt hữu ích khi một dòng có nhiều lời gọi liên tiếp.

Gợi ý «Did you mean»

Từ Python 3.10, khi gõ sai tên, thông báo lỗi đề xuất tên gần đúng. Trong Python 3.15, các gợi ý thông minh hơn: trình thông dịch nhận ra thói quen từ các ngôn ngữ khác. Gọi list.push sẽ gợi ý append, còn "cà phê".toUpperCase() sẽ gợi ý phương thức upper. Gợi ý cũng xét cả các thuộc tính lồng nhau: nếu dict nằm trong thuộc tính totals của một object và bạn gọi items() trực tiếp trên object đó, lỗi sẽ gợi ý .totals.items. Hàm delattr() cũng đã có gợi ý tên gần giống.

Các kiểu ngoại lệ trong Python

Có hàng chục ngoại lệ tích hợp, nhưng khi mới học bạn sẽ gặp chủ yếu những loại sau:

Ngoại lệKhi nào xuất hiệnVí dụ
NameErrorTên chưa được định nghĩa, thường do gõ saiprint(tong_tien)
TypeErrorPhép toán không phù hợp với kiểu này"5" + 3
ValueErrorKiểu đúng, nhưng giá trị không hợp lệint("mười")
KeyErrorKhông có khóa đó trong dictd["thieu"]
IndexErrorChỉ số nằm ngoài list[1, 2][5]
AttributeErrorObject không có phương thức hoặc thuộc tính đó[].push(1)
ZeroDivisionErrorChia cho 01 / 0
FileNotFoundErrorKhông tìm thấy tệpopen("khong_co.txt")

Riêng SyntaxError (và trường hợp đặc biệt IndentationError) xuất hiện ngay khi đọc tệp, trước khi chạy code, nên không thể bắt nó bằng try trong cùng tệp đó.

Cây phân cấp ngoại lệ

Ngoại lệ là các class và được tổ chức thành cây phân cấp. Ở gốc là BaseException. Từ đó kế thừa Exception (hầu hết các lỗi «thông thường») và một số lỗi hệ thống: KeyboardInterrupt, SystemExit, GeneratorExit.

BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError

Cây phân cấp quan trọng khi bắt lỗi: except LookupError bắt được cả KeyError lẫn IndexError. Còn KeyboardInterrupt không phải lỗi trong code: nó báo rằng người dùng đã nhấn Ctrl+C. Nó không kế thừa từ Exception một cách có chủ đích, để except Exception không chặn được việc dừng chương trình.

Xử lý ngoại lệ: try/except/else/finally

Đoạn code có thể gây lỗi được đặt trong khối try. Trong khối except, bạn chỉ rõ kiểu lỗi và việc cần làm khi gặp nó. Chính except là khối được Python dùng để xử lý ngoại lệ.

def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"Bỏ qua dòng: {e}")
        return None
    else:
        return value          # chạy khi không có lỗi
    finally:
        print("đã phân tích xong")   # luôn chạy

print(parse_amount("250"))

Thứ tự in ra ở đây là: đầu tiên «đã phân tích xong», sau đó là 250.0. return trong khối else ghi nhớ giá trị, sau đó Python chắc chắn chạy finally rồi mới trả kết quả. Vì vậy finally đáng tin cậy để dọn dẹp: đóng tệp, giải phóng kết nối.

Nếu viết một return riêng trong finally, nó sẽ ghi đè kết quả trước đó và thậm chí «nuốt» ngoại lệ. Từ Python 3.14, code kiểu này sinh ra SyntaxWarning (PEP 765) — đừng viết return trong finally.

Bắt nhiều ngoại lệ cùng lúc

Có thể viết nhiều khối except cho các kiểu khác nhau, hoặc liệt kê các kiểu trong một tuple:

try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("Số tiền không hợp lệ:", e)
    amount = 0

Các khối được kiểm tra từ trên xuống, khối khớp đầu tiên được thực thi, vì vậy các kiểu cụ thể nên đặt lên trên các kiểu chung. Từ Python 3.14 (PEP 758), có thể liệt kê nhiều kiểu mà không cần ngoặc đơn — except ValueError, TypeError: — nhưng khi có as thì vẫn bắt buộc phải có ngoặc.

Cách in thông báo lỗi và lấy kiểu lỗi

Đối tượng ngoại lệ có sẵn qua as e. str(e) cho thông báo, type(e).__name__ cho tên kiểu:

try:
    int("mười")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: 'mười'

Module traceback: toàn bộ nội dung lỗi

Đôi khi cần bắt lỗi nhưng vẫn giữ nguyên toàn bộ traceback — để ghi log hoặc báo cáo. Module traceback phục vụ việc này:

import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # in traceback ra stderr
    text = traceback.format_exc()     # cùng nội dung dưới dạng chuỗi

Nếu ghi log bằng module logging, cách đơn giản hơn là gọi logging.exception("không tính được tổng") bên trong except — traceback sẽ tự động được thêm vào thông báo.

Lệnh raise trong Python: gây ngoại lệ

Lệnh raise ném một ngoại lệ bằng tay. Nhờ đó hàm cho biết nó không thể xử lý dữ liệu như vậy:

def add_expense(amount):
    if amount < 0:
        raise ValueError(f"Số tiền không thể âm: {amount}")
    ...

add_expense(-5)
# ValueError: Số tiền không thể âm: -5

raise trong Python là một lệnh chứ không phải hàm, vì vậy sau từ raise không cần dấu ngoặc: các đối số được truyền vào hàm khởi tạo của ngoại lệ bên trong dấu ngoặc. Chọn kiểu theo ý nghĩa: giá trị sai là ValueError, kiểu sai là TypeError.

raise không có đối số và raise from

Bên trong except, một raise trơn sẽ chuyển tiếp ngoại lệ hiện tại mà không thay đổi gì. Điều này hữu ích khi bạn muốn ghi log nhưng vẫn để code gọi bên ngoài xử lý tiếp.

Nếu thay một ngoại lệ bằng ngoại lệ khác, hãy dùng raise ... from e — lỗi gốc được giữ lại trong chuỗi, và traceback thể hiện cả hai:

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"Không có tệp cấu hình {path}") from e

add_note: thêm ngữ cảnh cho lỗi

Từ Python 3.11 (PEP 678), có thể thêm ghi chú vào ngoại lệ bằng phương thức add_note() rồi tiếp tục ném nó. Kiểu lỗi không đổi, còn ghi chú được in ngay dưới thông báo:

def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"tệp chi_tieu.csv, dòng {line_no}")
        raise

parse("mười", 17)
# ValueError: could not convert string to float: 'mười'
# tệp chi_tieu.csv, dòng 17

Cách tạo ngoại lệ riêng trong Python

Ngoại lệ riêng cần cho các lỗi thuộc nghiệp vụ: vượt ngân sách, đơn hàng đã thanh toán, người dùng bị khóa. Chỉ cần một class kế thừa Exception:

class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: hạn mức {limit}, đã chi {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("Ăn uống", 10000, 12500)
except BudgetExceeded as e:
    print("Cảnh báo:", e)   # Cảnh báo: Ăn uống: hạn mức 10000, đã chi 12500

Bây giờ code gọi bắt đúng lỗi vượt ngân sách và không nhầm nó với lỗi gõ sai. Trong dự án lớn, nên tạo một class cơ sở chung (class TrackerError(Exception)) và cho mọi ngoại lệ riêng kế thừa từ đó — khi ấy có thể bắt tất cả bằng một except TrackerError. Hãy kế thừa từ Exception, không phải từ BaseException.

Nhóm ngoại lệ và except*

Đôi khi có nhiều lỗi cùng lúc — ví dụ khi kiểm tra cả một tệp, hoặc trong các tác vụ song song với asyncio.TaskGroup. Từ Python 3.11, chúng có thể được gom lại thành ExceptionGroup, và xử lý theo kiểu bằng except*:

def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("lỗi trong tệp", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"Số dòng sai: {len(eg.exceptions)}")   # Số dòng sai: 2

Python try except bắt tất cả lỗi: vì sao không nên

Thói quen tệ nhất là bắt mọi thứ: except: pass. Chương trình «không còn đổ lỗi», nhưng thực tế là không còn báo cáo vấn đề nữa. except trơn bắt cả KeyboardInterrupt và SystemExit, nên không thể dừng chương trình bằng Ctrl+C. except Exception: pass đỡ hơn một chút, nhưng vẫn nuốt im lặng các lỗi gõ sai tên và lỗi logic.

Cách xử lý ngoại lệ đúng trong Python:

  • chỉ bắt kiểu lỗi mà bạn biết xử lý: except ValueError, không bắt tất cả;
  • giữ khối try ngắn — chỉ những dòng có thể gây lỗi đúng kiểu đó;
  • nếu bắt rộng (except Exception) ở tầng cao nhất của chương trình, nhất định phải ghi log traceback;
  • đừng dùng ngoại lệ để rẽ nhánh thông thường khi kiểm tra đơn giản là đủ: dùng dict.get() thay vì try/except KeyError trong các trường hợp đơn giản;
  • với tài nguyên, ưu tiên with — nó tự đóng tệp ngay cả khi có lỗi.

Tổng kết

Ngoại lệ trong Python là cơ chế bình thường để báo lỗi, không phải lý do để giấu lỗi. Đọc traceback từ dưới lên, bắt các kiểu cụ thể trong try/except, dùng else cho code «khi mọi thứ thành công» và finally cho việc dọn dẹp. Lệnh raise cho phép bạn tự báo lỗi, raise from và add_note giữ được ngữ cảnh, còn ngoại lệ riêng kế thừa từ Exception. Chủ đề này được thực hành trong khóa học Python: trình theo dõi chi tiêu không còn sập vì một dòng CSV sai, và việc vượt ngân sách được biểu diễn bằng một ngoại lệ riêng.

Câu hỏi thường gặp

Lỗi và ngoại lệ trong Python khác nhau thế nào?

Trong lời nói thường ngày chúng là một. Về kỹ thuật, SyntaxError được phát hiện ngay khi đọc code, còn các lỗi khác xảy ra khi chạy dưới dạng đối tượng ngoại lệ có thể bắt được.

Sau khi bắt ngoại lệ, có thể tiếp tục từ đúng dòng bị lỗi không?

Không. Sau khi bắt, chương trình tiếp tục ở phía sau khối try/except, không phải ở dòng gây lỗi. Nếu cần xử lý từng dòng của tệp, hãy đặt try bên trong vòng lặp.

Chuyện gì xảy ra nếu ngoại lệ phát sinh bên trong except?

Ngoại lệ mới được ném tiếp, còn ngoại lệ gốc được giữ trong thuộc tính __context__. Traceback sẽ hiện câu During handling of the above exception, another exception occurred.

Làm sao kiểm tra trong test rằng hàm ném ngoại lệ?

Trong pytest dùng with pytest.raises(ValueError):, trong unittest dùng self.assertRaises(ValueError). Test chỉ đạt khi bên trong khối có ngoại lệ đúng kiểu được ném ra.

assert là gì và khác raise ở đâu?

assert điều_kiện, "thông báo" ném AssertionError khi điều kiện sai. Đây là công cụ kiểm tra trong lúc phát triển: khi chạy với cờ -O, các assert bị tắt, nên kiểm tra dữ liệu người dùng cần dùng if và raise.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Python 3.15: от новичка до профи”.

Đến khóa học