Đọc thông báo lỗi — dòng nào đáng tin, dòng nào không
Traceback chỉ đúng chỗ máy vấp, nhưng chỗ vấp thường không phải chỗ bạn viết sai. Đọc từ dưới lên, và biết khi nào phải lần ngược lên chỗ gọi.
TL;DR: Chương trình dừng lại, màn hình hiện bảy dòng traceback kết thúc bằng TypeError — nhưng dòng bị nêu tên không hề sai. Đọc traceback từ dưới lên: dòng cuối nói loại lỗi, các dòng File ..., line N phía trên là dấu vết lời gọi máy đã đi qua. Dòng gần đáy nhất chỉ đúng chỗ máy VẤP, không nhất thiết là chỗ viết sai — giá trị gây lỗi có thể tới từ một lời gọi hàm ở xa hơn. Quy trình ba bước: đọc loại lỗi ở dòng cuối, lần lên tìm dòng code của chính bạn gần chỗ vấp nhất, rồi hỏi giá trị đó tới từ đâu.
Bạn viết xong một hàm tính điểm trung bình cho nhóm bốn bạn, chạy thử. Màn hình không hiện con số nào cả — nó hiện bảy dòng chữ, dòng cuối có một cái tên lạ hoắc: TypeError.
Bạn nhìn vào đúng dòng code vừa bị nêu tên. Dòng đó chỉ cộng hai biến lại với nhau, nhìn mãi cũng không thấy có gì sai. Bạn đọc đi đọc lại thêm ba lần, vẫn không ra manh mối.
Traceback không nói dối, nhưng chỗ nó chỉ vào chỉ là chỗ máy DỪNG LẠI — dòng đó và dòng bạn cần sửa có thể là hai dòng khác nhau.
Ở bài đó, bạn học đọc đúng DÒNG CUỐI của một traceback để biết loại lỗi — dòng TypeError: can only concatenate str (not "int") to str đủ để bạn biết str và int vừa va nhau. Bài này mở rộng ra toàn bộ traceback, cho những lần dòng bị báo không phải dòng bạn cần sửa.
Bài này lo đúng một việc: từ một traceback của lỗi runtime — loại có traceback, theo cách phân loại ở bài trước — tìm ra dòng code THẬT SỰ cần sửa, không dạy lại phân loại ba loại lỗi hay quy trình sửa bằng giả thuyết (bài sau lo việc đó).
1. Analogy — trạm cuối dây chuyền phát hiện hỏng, hàng có thể hỏng từ trạm đầu
Cứ hình dung một dây chuyền đóng gói bánh có ba trạm: trạm A đổ bột vào khuôn, trạm B nướng, trạm C mở gói kiểm tra. Trạm C phát hiện bánh vỡ vụn, báo lỗi tại chính trạm mình vì đó là nơi phát hiện ra — nhưng bánh có thể đã hỏng từ lúc trạm A đổ sai lượng bột, chỉ đợi tới lúc mở gói mới lộ ra.
| Đời thường | Traceback |
|---|---|
| Trạm cuối cùng mở gói, phát hiện hỏng | Dòng cuối traceback: tên loại lỗi + thông điệp |
| Phiếu chuyển ghi trạm nào giao cho trạm nào | Mỗi dòng File ..., line N là một lời gọi trong chuỗi |
| Trạm gần chỗ phát hiện hỏng nhất | Dòng File gần đáy traceback nhất — nơi máy vấp |
| Bánh có thể hỏng từ trạm đổ khuôn đầu tiên | Nguyên nhân thật có thể ở lời gọi ban đầu, không phải nơi máy dừng |
Máy chỉ báo NƠI NÓ DỪNG, không báo nơi NGUYÊN NHÂN bắt đầu. Hai nơi đó trùng nhau ở một dòng lệnh đơn giản, nhưng tách rời ngay khi có hàm gọi hàm.
2. Giải phẫu một traceback, đọc từ dưới lên
Traceback không phải một khối chữ đỏ ngẫu nhiên. Nó có cấu trúc cố định, và đọc đúng thứ tự là điều kiện cần trước khi hiểu nó nói gì.
Dòng đầu tiên luôn là Traceback (most recent call last):, một câu thông báo cố định. Dưới đó là các cặp dòng File "...", line N, in <tên> kèm dòng mã nguồn tương ứng — mỗi cặp là một LỜI GỌI máy đã đi qua. Dòng CUỐI CÙNG, đứng tách riêng, là tên loại lỗi và thông điệp — dòng DUY NHẤT nói CÁI GÌ đã sai.
Tên "most recent call last" không trang trí: cặp dòng gần ĐÁY nhất, ngay trên dòng lỗi, là lời gọi GẦN NHẤT với chỗ máy dừng — chỗ máy VẤP. Đọc từ dưới lên nghĩa là xem dòng lỗi trước, xem cặp dòng ngay trên để biết máy vấp ở đâu, rồi lần tiếp lên trên nếu cần biết máy tới đó bằng đường nào.

Hộp bên trong tinh_trung_binh là chỗ máy vấp: gần đáy nhất, sát dòng báo lỗi.
Traceback ở bài này chạy trên Python 3.14. Từ Python 3.11, một số dòng có thêm dấu ^^^^/~~~~ khoanh biểu thức con gây lỗi — bản cũ hơn không có dấu này, nhưng cấu trúc dòng cuối là loại lỗi, dòng File là dấu vết thì không đổi.
3. Vì sao chỗ máy vấp không phải chỗ bạn viết sai?
Nhóm bạn của bạn nhờ viết chương trình tính điểm trung bình. Ba bạn đầu đọc điểm và đổi sang số bằng int(input()) tử tế. Bạn thêm bạn thứ tư vào phút chót, gõ vội một dòng, quên mất bước đổi kiểu:
def tinh_trung_binh(danh_sach):
tong = 0
for diem in danh_sach:
tong = tong + diem
return tong / len(danh_sach)
diem_1 = int(input())
diem_2 = int(input())
diem_3 = int(input())
diem_4 = input()
danh_sach_diem = [diem_1, diem_2, diem_3, diem_4]
trung_binh = tinh_trung_binh(danh_sach_diem)
print(trung_binh)
Hàm tinh_trung_binh nhận tham số danh_sach qua lời gọi ở dòng cuối cùng. Bên trong hàm không hề biết giá trị đó tới từ đâu, nó chỉ nhận và xử lý. Chính ranh giới vào/ra đó là lý do chỗ máy vấp và chỗ bạn viết sai có thể là hai dòng khác nhau.
Chạy thử, nhập lần lượt 7, 9, 10, 8.
Chương trình có in ra điểm trung bình không? Nếu nó dừng lại giữa chừng, bạn nghĩ nó dừng ở dòng nào trong đoạn code trên?
Traceback (most recent call last):
File "bai.py", line 14, in <module>
trung_binh = tinh_trung_binh(danh_sach_diem)
File "bai.py", line 4, in tinh_trung_binh
tong = tong + diem
~~~~~^~~~~~
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Bước 1 — đọc dòng cuối. TypeError: unsupported operand type(s) for +: 'int' and 'str': phép + vừa gặp một int và một str, đúng thứ đã học ở bài Chuỗi và số — hai kiểu khác nhau, Python từ chối.
Bước 2 — tìm dòng gần chỗ vấp nhất. Cặp dòng ngay trên dòng lỗi là dòng bên trong tinh_trung_binh: tong = tong + diem — nơi máy đang chạy khi dừng lại. Dấu ~~~~~^~~~~~ khoanh đúng phép + gây lỗi.
Nhìn riêng dòng này không thấy gì sai: tong = tong + diem đúng cú pháp, đúng logic, chỉ cộng một số vào số khác. Đây là chỗ dễ khựng lại — máy báo đúng dòng này, nhưng dòng này không viết sai.
Bước 3 — hỏi giá trị nào đi vào, từ đâu tới. diem là từng phần tử của danh_sach, tham số của hàm: theo Recall ở trên, hàm chỉ nhận, không biết giá trị tới từ đâu. Lần lên cặp dòng gọi tinh_trung_binh(danh_sach_diem), rồi lên tiếp bạn thấy diem_4 = input() — thiếu đúng một chữ int(...) so với ba dòng trên nó. Đó, không phải dòng cộng bên trong hàm, mới là dòng cần sửa.
4. Thử đoán — traceback thứ hai
Bạn viết một hàm đếm số từ trong một câu, tận dụng lại nguyên tắc vào-xử lý-ra đã quen ở module 3:
def dem_so_tu(cau):
cac_tu = cau.split()
return len(cac_tu)
tuoi = int(input())
so_tu = dem_so_tu(tuoi)
print(so_tu)
Trước khi đọc tiếp: dòng tuoi = int(input()) đã đổi kết quả input() thành số, rồi chương trình gọi dem_so_tu(tuoi) ngay sau đó. Đoán xem — loại lỗi (tên exception) sẽ là gì, và cặp dòng gần đáy traceback nhất sẽ là dòng nào trong hai hàm bên trên?
Chạy trên Python 3.14, nhập 15 ở dòng input():
Traceback (most recent call last):
File "bai.py", line 7, in <module>
so_tu = dem_so_tu(tuoi)
File "bai.py", line 2, in dem_so_tu
cac_tu = cau.split()
^^^^^^^^^
AttributeError: 'int' object has no attribute 'split'
5. Tới lượt bạn — tự áp dụng quy trình ba bước
Lấy giấy, dùng đúng ba bước ở mục 3 cho traceback vừa hiện ở trên. Viết ra ba câu trả lời:
- Dòng cuối nói loại lỗi gì, và thông điệp cụ thể là gì?
- Cặp dòng gần đáy nhất (ngay trên dòng lỗi) là dòng nào — nhìn riêng dòng đó, nó có sai cú pháp hay logic không?
- Giá trị nào đi vào dòng đó, và lần lên trên bạn tìm thấy nó từ đâu tới?
Đáp án đầy đủ nằm ở mục 10, ngay trước phần tự kiểm tra — chỉ mở sau khi đã tự viết xong cả ba câu.
Nhìn lướt rồi tự nhủ "chắc hiểu rồi" mà không viết ra ba câu trả lời là bạn đang bỏ qua đúng bước quan trọng nhất bài này dạy.
6. Bẫy thường gặp
❌ Nhầm 1 — sửa ngay dòng bị traceback nêu tên: thấy File, line N chỉ vào một dòng là sửa luôn, không kiểm tra dòng đó có thật sự sai không. Với ví dụ mục 3, sửa nhầm cách hàm cộng — trong khi hàm cộng không có gì sai — thì lỗi vẫn còn, chỉ đổi dạng.
✅ Luôn hỏi "nhìn riêng dòng này, nó có sai không?" trước khi sửa. Không sai thì lần tiếp lên chỗ gọi.
❌ Nhầm 2 — đọc traceback từ trên xuống: cố hiểu ngay dòng đầu tiên vì nó nằm trên cùng. Dòng đó thường là entry point xa nhất, mức module, không liên quan trực tiếp tới nguyên nhân — cố hiểu nó trước chỉ gây rối.
✅ Luôn bắt đầu từ dòng cuối lấy loại lỗi, rồi đi ngược lên.
7. 📚 Đào sâu — carets chỉ đúng biểu thức con
PEP 657 — Include Fine Grained Error Locations in Tracebacks, áp dụng từ Python 3.11, là lý do traceback bài này có dấu ^^^^/~~~~. Trước đó Python chỉ biết SỐ DÒNG gây lỗi, không đủ chính xác khi một dòng có nhiều phép toán lồng nhau. PEP 657 lưu thêm vị trí cột cho từng chỉ thị bytecode, nên Python khoanh đúng biểu thức con gây lỗi thay vì cả dòng. Bản trước 3.11 không có dấu này — traceback vẫn đúng, chỉ kém chi tiết hơn.
8. Liên hệ các bài khác
- Bài 1 — Ba loại lỗi — traceback chỉ xuất hiện ở loại lỗi runtime; bài đó giúp bạn nhận ra khi nào thật sự cần đọc kỹ một traceback.
- Bài 3 module 1 — Chuỗi và số — nơi bạn học đọc dòng cuối của traceback lần đầu tiên.
- Bài 5 module 3 — Hàm — ranh giới tham số và giá trị trả về chính là lý do chỗ vấp và chỗ sai tách rời nhau.
- Bài 3 — Sửa bằng giả thuyết — sau khi tìm ra dòng nghi ngờ, bài sau dạy cách kiểm chứng trước khi sửa.
9. Tóm tắt
- Chỗ máy vấp lệch chỗ viết sai vì ranh giới hàm: hàm nhận giá trị qua tham số, không biết nó từ đâu tới.
- Cặp dòng gần đáy nhất chỉ đúng NƠI máy vấp — nhìn riêng dòng đó thường không thấy gì sai, phải lần lên chỗ gọi để tìm giá trị gây lỗi.
- Sửa nhầm dòng bị nêu tên thay vì dòng gây ra thật là bẫy phổ biến nhất; traceback dài hơn chỉ có nghĩa chuỗi gọi dài hơn, không có nghĩa lỗi nặng hơn.
- Dấu
^^^^(Python 3.11+) khoanh đúng biểu thức con gây lỗi, nhưng không thay được việc tự lần theo lời gọi.
10. Đáp án — quy trình ba bước cho traceback thứ hai
- Dòng cuối:
AttributeError: 'int' object has no attribute 'split'— kiểuintkhông có methodsplit,splitchỉ có ởstr. - Cặp dòng gần đáy nhất là dòng bên trong
dem_so_tu:cac_tu = cau.split(). Nhìn riêng dòng này không có gì sai —.split()là thao tác hợp lệ nếucauđúng là một chuỗi. - Giá trị đi vào là
cau, tham số của hàm. Lần lên, bạn thấy lời gọidem_so_tu(tuoi), và phía trên nótuoi = int(input())đã đổi biến này thành số ngay từ đầu. Dòng cần sửa là dòng gọi hàm — nó cần một chuỗi, nhưng bạn lại truyền vào một số.
Đối chiếu ba câu bạn tự viết ở mục 5 với đáp án trên. Lệch ở bước 2 hoặc 3 — nghĩa là bạn dừng ở dòng bị nêu tên thay vì lần lên chỗ gọi — đọc lại mục 3.
11. Tự kiểm tra
- Q1Vì sao đọc traceback nên bắt đầu từ dòng cuối cùng thay vì dòng đầu tiên?
- Q2Trong ví dụ tinh_trung_binh, dòng bị traceback báo là tong = tong + diem, nhưng dòng cần sửa lại là diem_4 = input(). Giải thích cơ chế vì sao hai dòng đó khác nhau.
- Q3
Traceback dưới đây từ một chương trình tính giá sau giảm, đã chạy thật:
Traceback (most recent call last): File "bai.py", line 7, in <module> gia_sau_giam = ap_dung_giam_gia(gia, phan_tram) File "bai.py", line 2, in ap_dung_giam_gia return gia * (1 - phan_tram) ~~~~^~~~~~~~~~~~~~~~~ TypeError: can't multiply sequence by non-int of type 'float'Áp dụng đúng ba bước ở mục 3: dòng cuối nói gì, dòng gần đáy nhất là dòng nào (nhìn riêng nó có sai không), và giá trị nào đi vào dòng đó khiến phép nhân gãy — bạn nghi ngờ dòng nào thật sự cần sửa?
- Q4Nếu đổi lời gọi thành dem_so_tu(str(tuoi)) thay vì dem_so_tu(tuoi), traceback ở trên còn xuất hiện không? Vì sao?
- Q5Một người bạn nói: "traceback dài quá, chắc lỗi này khó lắm, chắc phải sửa nhiều chỗ." Bạn phản biện thế nào dựa trên bài học hôm nay?
Bài tiếp theo: Sửa bằng giả thuyết, không sửa mò
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên