Xử lý lỗi finlens: FinLensError và cây ngoại lệ Python
Cách bắt lỗi khi lấy dữ liệu chứng khoán bằng finlens: except FinLensError bắt trọn cây ngoại lệ, rẽ nhánh theo mã FL_*, dùng on_error cho lời gọi nhiều mã.
Mọi lỗi thư viện finlens chủ động sinh ra đều nằm trong một cây thừa kế duy
nhất, gốc là finlens.FinLensError. Trang này nói bạn nên bắt ở mức nào, đọc gì
từ ngoại lệ bắt được, và phân biệt lỗi (dừng chương trình) với cảnh báo (không
dừng).
Một dòng bắt hết
import finlens
client = finlens.client() # đọc FINLENS_API_KEY
try:
df = client.eod.stock.ohlcv("HPG", start="2024-01-01", end="2024-06-30")
except finlens.FinLensError as exc:
print(exc.code, "-", exc.message)`except finlens.FinLensError` là đủ
Đây là cam kết của bản 1.x và có test khẳng định: mọi lớp lỗi công khai của
thư viện đều là con của FinLensError.
Tài liệu của bản 0.1.x nói ngược lại — rằng except FinLensError để lọt
CompanyTypeMismatchError và các lỗi bị tầng dòng tiền ném lại thành
RuntimeError. Cả hai chỗ đó đã được sửa. Nếu code của bạn còn một nhánh
except RuntimeError viết theo hướng dẫn cũ, nhánh đó nay là code chết — bỏ đi
được.
Không cần except Exception. Bắt rộng như vậy sẽ nuốt luôn KeyboardInterrupt
của bạn, lỗi gõ sai tên cột, và mọi thứ khác không liên quan đến FinLens.
Ngoại lệ mang theo những gì
Mọi lớp trong cây đều có sáu thuộc tính sau:
| Thuộc tính | Kiểu | Nội dung |
|---|---|---|
.code | str | Mã lỗi ổn định, máy đọc được — ví dụ "FL_QUOTA_DAILY" |
.message | str | Mô tả cho người đọc, tiếng Việt |
.request_id | str | None | Mã của request thất bại. Gửi kèm khi báo lỗi |
.status_code | int | None | Mã trạng thái HTTP, nếu lỗi đến từ một response |
.detail | Any | Dữ liệu cấu trúc kèm theo, nếu có |
.doc_url | str | Trang tài liệu của mã lỗi này |
str(exc) gộp ba thứ quan trọng nhất lại:
[FL_QUOTA_DAILY] Đã dùng hết hạn mức 5000 request/ngày. Đặt lại lúc 2026-08-05T00:00:00+07:00. (request_id=req_8f3a1c) -> https://docs.finlens.vn/python-sdk/errors/FL_QUOTA_DAILYRẽ nhánh theo `.code`, không bao giờ theo `.message`
code là hợp đồng: nó không đổi, không được dịch. message thì được phép đổi
chữ và được phép dịch bất cứ lúc nào.
try:
df = client.eod.stock.ohlcv("HPG")
except finlens.QuotaError as exc:
if exc.code == "FL_QUOTA_DAILY": # đúng
...
if "hạn mức" in exc.message: # sai — sẽ hỏng khi câu chữ đổi
...Một số lớp mang thêm thuộc tính riêng:
| Lớp | Thuộc tính thêm |
|---|---|
TierError và các con | .account_type (gói hiện tại), .required_tier (gói tối thiểu cần có) |
RateLimitError | .retry_after — số giây nên chờ |
DailyQuotaExceededError | .resets_at — thời điểm hạn mức đặt lại, chuỗi kèm offset +07:00 |
InvalidDateRangeError | .max_days — số ngày tối đa server chấp nhận |
PartialFetchError | .data (phần lấy được) và .failures (mã → ngoại lệ) |
Cây ngoại lệ
FinLensError
├── ConfigurationError cấu hình client sai: thiếu khoá, base URL hỏng
├── UnsupportedOperationError có trong API nhưng chưa khả dụng ở bản này
│
├── AuthError không xác thực được khoá
│ ├── InvalidApiKeyError khoá không tồn tại hoặc đã bị thu hồi
│ ├── ApiKeyExpiredError khoá hết hạn
│ ├── AccountExpiredError gói dịch vụ hết hạn (khoá vẫn còn hiệu lực)
│ └── AccountInactiveError tài khoản inactive hoặc banned
│
├── TierError gói hiện tại không cho phép thao tác này
│ ├── DatasetNotInTierError bộ dữ liệu ngoài gói
│ └── SymbolNotInTierError mã chứng khoán ngoài gói
│
├── QuotaError chạm một giới hạn sử dụng
│ ├── RateLimitError vượt giới hạn tần suất
│ └── DailyQuotaExceededError hết hạn mức request trong ngày
│
├── ValidationError tham số truyền vào không hợp lệ
│ ├── InvalidSymbolError mã sai định dạng hoặc không tồn tại
│ ├── InvalidDateRangeError khoảng thời gian sai hoặc quá dài
│ ├── InvalidIntervalError interval không hợp lệ hoặc nhập nhằng
│ ├── CompanyTypeMismatchError các mã thuộc nhiều loại hình doanh nghiệp
│ └── UnitMismatchError ghép các bảng khác đơn vị
│
├── TransportError lỗi mạng, trước khi có response hợp lệ
│ ├── ConnectionFailedError không kết nối được
│ │ └── TlsVerificationError không xác thực được chứng chỉ TLS
│ ├── RequestTimeoutError hết thời gian chờ
│ └── ServerError backend trả 5xx sau khi đã thử lại
│
├── DataError có response nhưng nội dung không đúng hợp đồng
│ ├── DecodingError không giải mã được payload
│ ├── SchemaMismatchError server trả schema client không hiểu
│ ├── NoDataError tài nguyên không tồn tại
│ └── PartialFetchError một phần các mã thất bại
│
├── UpgradeRequiredError bản client này không còn được hỗ trợ
└── IncompatibleApiVersionError client và backend khác major của hợp đồngAuthError, TierError, QuotaError, ValidationError, TransportError và
DataError vừa là lớp cha vừa là lớp bắt được: bắt lớp cha là bắt trọn nhánh,
kể cả những lớp con được thêm về sau. TlsVerificationError là ví dụ — nó là
lớp con của ConnectionFailedError, nên code chỉ bắt lớp cha vẫn nhận được nó
mà không phải sửa gì.
Nên bắt ở mức nào
Quy tắc ba dòng
Bắt lớp cha khi bạn chỉ cần biết "có lỗi". Bắt lớp lá khi mỗi loại lỗi
dẫn tới một hành động khác nhau. Phân biệt bằng .code khi cùng một lớp mà
hai mã cần hai cách xử lý.
| Bạn đang viết gì | Khuyến nghị |
|---|---|
| Notebook, script phân tích chạy một lần | Một khối except finlens.FinLensError, in exc ra rồi dừng. Đủ dùng |
| Job chạy định kỳ, không có người trực | Tách ba nhóm: thử lại được, cần người can thiệp, lỗi trong code của bạn |
| Ứng dụng phục vụ người khác | Bắt FinLensError ở biên, hiển thị exc.message, ghi log exc.code và exc.request_id |
| Lời gọi nhiều mã | Đừng bọc try. Dùng tham số on_error — xem mục dưới |
Bảng phân loại cho nhóm job định kỳ:
| Nhóm | Lớp | Nên làm gì |
|---|---|---|
| Thử lại được | RateLimitError, RequestTimeoutError, ServerError, ConnectionFailedError | Chờ rồi chạy lại. Với RateLimitError thì chờ đúng .retry_after giây |
| Chờ vô ích | TlsVerificationError, AuthError, TierError, UpgradeRequiredError | Báo cho người. Nguyên nhân nằm ở máy bạn, ở tài khoản, hoặc ở phiên bản — thử lại không đổi được gì |
| Hết hạn mức | DailyQuotaExceededError | Chạy lại sau .resets_at, không sớm hơn |
| Lỗi trong code của bạn | ValidationError và các con | Sửa code. Chạy lại y nguyên sẽ ra đúng lỗi đó |
Hai dòng đầu giao nhau ở một chỗ: TlsVerificationError là một
ConnectionFailedError. Nếu bạn bắt lớp cha để thử lại thì phải đặt nhánh
TlsVerificationError lên trước, không thì một lỗi vĩnh viễn sẽ bị thử lại
mãi mà không bao giờ khác đi.
import time
import finlens
client = finlens.client()
try:
df = client.eod.stock.investor.flow("HPG", start="2024-01-01")
except finlens.RateLimitError as exc:
time.sleep(exc.retry_after or 60)
except finlens.DailyQuotaExceededError as exc:
print("Hạn mức đặt lại lúc", exc.resets_at)
except finlens.AuthError as exc:
print("Kiểm tra lại khoá API:", exc.message)
except finlens.FinLensError as exc:
print(exc.code, exc.message, exc.request_id)Thứ tự except đi từ hẹp tới rộng. Đặt FinLensError lên trên cùng thì mọi
nhánh phía dưới không bao giờ chạy.
ValidationError vẫn là ValueError
ValidationError kế thừa cả FinLensError và ValueError, và đó là
chủ đích: code cũ viết except ValueError quanh lời gọi FinLens vẫn tiếp tục
chạy sau khi nâng lên 1.x.
try:
df = client.eod.stock.ohlcv("HPG", interval="1M")
except ValueError as exc: # vẫn bắt được
print(exc)Nhưng khi viết mới thì bắt finlens.ValidationError: nó chỉ bắt lỗi tham số của
FinLens, không nuốt luôn ValueError từ code khác của bạn.
Ba lỗi tham số hay gặp nhất:
InvalidIntervalError—intervalchỉ nhận1d,1w,1mo,3mo,6mo,1y.1Mvà1mbị từ chối thẳng thay vì đoán ý: trong từ vựng cũ hai token đó chỉ khác nhau ở chữ hoa nhưng lệch nhau 43.200 lần.InvalidDateRangeError— khoảng thời gian vượt giới hạn. Giới hạn thật do server quyết định, đọc bằngclient.limits(), không phải hằng số trong thư viện.CompanyTypeMismatchError— các mã thuộc nhiều loại hình doanh nghiệp khác nhau (phi tài chính, ngân hàng, chứng khoán, bảo hiểm). Cây chỉ tiêu báo cáo tài chính của chúng khác nhau nên không gộp vào một bảng được.
Nhiều mã trong một lời gọi: on_error
Khi bạn xin 50 mã và 1 mã hỏng, 49 mã còn lại không được mất theo. Đó là việc
của tham số on_error, có ở mọi hàm nhận nhiều mã.
| Giá trị | Hành vi |
|---|---|
"warn" (mặc định) | Giữ phần thành công, phát PartialDataWarning. Chương trình chạy tiếp |
"raise" | Ném PartialFetchError — mang theo phần đã lấy được ở .data |
"ignore" | Giữ phần thành công, không nói gì cả |
Cả ba trường hợp đều ghi lại chi tiết vào df.attrs["finlens"]:
df = client.eod.stock.ohlcv(ma_list, start="2024-01-01") # on_error="warn"
if df.attrs["finlens"]["partial"]:
for symbol, ly_do in df.attrs["finlens"]["failed"].items():
print(symbol, ly_do["code"], ly_do["message"])Với on_error="raise", dữ liệu đi kèm ngoại lệ nên bạn không phải gọi lại từ đầu:
try:
df = client.eod.stock.ohlcv(ma_list, on_error="raise")
except finlens.PartialFetchError as exc:
df = exc.data # 49 mã thành công, vẫn dùng được
print(exc.failures) # {"XYZ": InvalidSymbolError(...)}exc.failures ánh xạ mã chứng khoán → ngoại lệ tương ứng, nên mỗi mã hỏng
vẫn giữ đủ .code và .message của riêng nó.
`df.attrs` không sống sót qua `merge` và `concat`
pandas không chuyển attrs qua các phép ghép bảng. Đọc partial, failed,
units trước khi ghép, hoặc lưu lại giá trị bạn cần. Đây là thông tin đi
kèm dữ liệu, không phải một cơ chế bảo vệ.
Kết quả không có dòng nào không phải lỗi. Một khoảng ngày nghỉ, hay một mã
mới niêm yết, trả về DataFrame rỗng với đúng cột và đúng dtype — không cảnh báo,
không ngoại lệ. NoDataError chỉ dành cho trường hợp server báo rõ rằng tài
nguyên không tồn tại.
TlsVerificationError: máy bạn nằm sau proxy kiểm tra TLS
Rất phổ biến trong mạng doanh nghiệp: proxy kiểm tra TLS hoặc phần mềm diệt virus chèn vào giữa kết nối bằng một CA riêng, mà bundle chứng chỉ mặc định của Python không có CA đó.
Đây là lỗi vĩnh viễn, khác hẳn một lỗi kết nối thường: thử lại không bao giờ cứu được, và cách sửa nằm ở máy bạn chứ không ở phía FinLens. Vì vậy thư viện không thử lại nó.
client = finlens.client(ca_bundle="/duong/dan/toi/ca-bundle.pem")Hoặc đặt biến môi trường một lần cho cả máy:
# Linux / macOS
export FINLENS_CA_BUNDLE="/duong/dan/toi/ca-bundle.pem"
# Windows PowerShell
$env:FINLENS_CA_BUNDLE = "C:\certs\ca-bundle.pem"File CA bundle do bộ phận IT của bạn cấp — đó là chứng chỉ gốc của chính proxy đang đứng giữa.
Khác với 0.1.x
Ở bản 0.1.x, lỗi chứng chỉ bị báo cáo thành "backend đang không phản hồi", gửi
người đọc đi kiểm đúng chỗ không có vấn đề gì. Nếu bạn đang gặp thông báo đó
trong khi curl cùng lúc vẫn chạy, hãy nâng cấp bằng pip install -U finlens
rồi đọc lại thông báo mới.
TlsVerificationError kế thừa ConnectionFailedError, nên code đang bắt lớp
cha không phải sửa gì.
SchemaMismatchError: response sai hình dạng
Ném ra khi server trả về một cấu trúc mà phiên bản client này không hiểu. Hai nguyên nhân, theo thứ tự khả năng:
FINLENS_BASE_URLđang trỏ nhầm chỗ — thường là một endpoint cũ hoặc một backend cục bộ đang chạy phiên bản khác. Kiểm biến môi trường trước tiên.- Client quá cũ so với backend. Nâng cấp bằng
pip install -U finlens.
Đây là thay đổi hành vi so với 0.1.x
Ở bản 0.1.x, một response sai hình dạng trả về bảng rỗng trong im lặng. Người dùng đọc "khối ngoại mua ròng 0 đồng" trong khi thật ra client không hiểu được response — không ngoại lệ, không cảnh báo.
Nay nó ném lỗi. Một traceback to rõ ràng tốt hơn một con số sai trông rất thật.
Bảng tra mã lỗi
Ánh xạ là nhiều-đối-một: nhiều mã có thể trỏ về cùng một lớp. Bắt theo lớp, phân biệt theo mã.
| Mã | Lớp ngoại lệ | Thông báo bạn thấy |
|---|---|---|
FL_CONFIG | ConfigurationError | Cấu hình client không hợp lệ |
FL_AUTH_INVALID_KEY | InvalidApiKeyError | API key không hợp lệ hoặc đã bị thu hồi |
FL_AUTH_KEY_EXPIRED | ApiKeyExpiredError | API key đã hết hạn vào ... |
FL_AUTH_ACCOUNT_EXPIRED | AccountExpiredError | Gói FinLens của bạn đã hết hạn vào ... Vui lòng gia hạn để tiếp tục |
FL_AUTH_ACCOUNT_INACTIVE | AccountInactiveError | Tài khoản đang ở trạng thái ... |
FL_TIER_DATASET | DatasetNotInTierError | Bộ dữ liệu ... cần gói ... (bạn đang dùng ...) |
FL_TIER_SYMBOL | SymbolNotInTierError | Mã ... cần gói ... (bạn đang dùng ...) |
FL_QUOTA_RATE | RateLimitError | Vượt giới hạn tần suất. Thử lại sau ... giây |
FL_QUOTA_DAILY | DailyQuotaExceededError | Đã dùng hết hạn mức ... request/ngày. Đặt lại lúc ... |
FL_VALIDATION_SYMBOL | InvalidSymbolError | Mã ... không tồn tại |
FL_VALIDATION_DATE_RANGE | InvalidDateRangeError | Khoảng thời gian quá lớn: yêu cầu ... ngày, tối đa ... |
FL_VALIDATION_INTERVAL | InvalidIntervalError | Interval ... không hợp lệ. Giá trị hợp lệ: ... |
FL_VALIDATION_INTERVAL_AMBIGUOUS | InvalidIntervalError | Interval ... nhập nhằng: có thể là một phút hoặc một tháng |
FL_VALIDATION_COMPANY_TYPE | CompanyTypeMismatchError | Các mã thuộc nhiều loại hình doanh nghiệp ... |
FL_VALIDATION_UNIT_MISMATCH | UnitMismatchError | Không thể ghép các bảng khác đơn vị: ... và ... |
FL_DATA_EMPTY | NoDataError | Không có dữ liệu cho ... |
FL_DATA_SCHEMA | SchemaMismatchError | Server trả về schema mà phiên bản client này không hiểu |
FL_DATA_PARTIAL | PartialFetchError | .../... mã thất bại |
FL_TRANSPORT_CONNECT | ConnectionFailedError | Không kết nối được tới ... |
FL_TRANSPORT_TLS | TlsVerificationError | Xác thực chứng chỉ TLS thất bại khi nối tới ... |
FL_TRANSPORT_TIMEOUT | RequestTimeoutError | Request hết thời gian chờ sau ... giây |
FL_SERVER | ServerError | Lỗi phía server |
FL_VERSION_UPGRADE_REQUIRED | UpgradeRequiredError | Phiên bản client này không còn được hỗ trợ. Tối thiểu là ... |
FL_VERSION_API_INCOMPATIBLE | IncompatibleApiVersionError | Client dùng contract major ..., server dùng ... |
Dấu ... là chỗ thông báo thật điền giá trị cụ thể của lời gọi.
Một mã không có trong bảng này vẫn được ánh xạ về một lớp trong cây theo nhóm
của nó — không bao giờ thành một RuntimeError trần. Đó là lý do
except finlens.FinLensError giữ được lời hứa của mình kể cả khi backend thêm
mã mới.
Cảnh báo khác lỗi
Cảnh báo không dừng chương trình. Chúng đi qua module warnings của Python,
nên bạn tắt hay bật từng loại một cách chọn lọc.
FinLensWarning (UserWarning)
├── UpgradeAvailableWarning có bản client mới hơn trên PyPI
├── QuotaLowWarning hạn mức sắp cạn. Chỉ phát một lần mỗi client
├── PartialDataWarning một phần các mã thất bại, với on_error="warn"
├── DataQualityWarning dữ liệu trả về có vấn đề đã biết
└── TierLimitWarning yêu cầu đã bị cắt bớt cho vừa gói dịch vụ
FinLensDeprecationWarning (DeprecationWarning)
một phần của API công khai sắp bị gỡ bỏTắt một loại, giữ nguyên các loại còn lại:
import warnings
import finlens
warnings.filterwarnings("ignore", category=finlens.QuotaLowWarning)Hoặc làm ngược lại — biến cảnh báo thành lỗi để nó không thể bị bỏ qua. Hữu ích trong job chạy tự động, nơi không ai đọc stderr:
warnings.filterwarnings("error", category=finlens.PartialDataWarning)Lúc đó PartialDataWarning sẽ được ném ra như một ngoại lệ và dừng chương
trình — khác với on_error="raise" ở chỗ bạn không nhận được .data.
Tắt toàn bộ cảnh báo của FinLens mà không đụng tới cảnh báo của thư viện khác:
warnings.filterwarnings("ignore", category=finlens.FinLensWarning)Thư viện không còn tắt cảnh báo của bạn
Bản 0.1.x gọi warnings.filterwarnings("ignore") ngay lúc import, tức là tắt
mọi cảnh báo trong toàn bộ tiến trình của bạn — kể cả cảnh báo của pandas,
numpy và code của chính bạn. Bản 1.x không làm vậy nữa.
FinLensDeprecationWarning kế thừa DeprecationWarning chứ không kế thừa
FinLensWarning, theo đúng quy ước Python: mặc định ẩn với người dùng cuối,
hiện ra khi chạy python -W default.
Khi cần báo lỗi
Gửi kèm ba thứ, chúng cắt ngắn phần lớn cuộc trao đổi qua lại:
import finlens
print(finlens.__version__)
print(finlens.build_info()) # commit và thời điểm build của binary bạn đang chạyCộng thêm exc.request_id của lời gọi hỏng — đó là thứ cho phép tra lại đúng
request đó ở phía FinLens.
Danh sách thay đổi từng phiên bản: Nhật ký thay đổi.
Câu hỏi thường gặp
Bắt lỗi của thư viện finlens bằng cách nào?
Dùng `except finlens.FinLensError`. Ở bản 1.x mọi lớp lỗi công khai của thư viện Python finlens đều kế thừa `FinLensError`, nên một khối `except` là đủ và không cần `except Exception`. Ngoại lệ bắt được mang `.code` (mã ổn định như `FL_QUOTA_DAILY`), `.message` bằng tiếng Việt, `.request_id`, `.status_code`, `.detail` và `.doc_url`; `str(exc)` gộp sẵn ba thứ quan trọng nhất. Luôn rẽ nhánh theo `.code`, đừng so khớp `.message` — nội dung thông báo được phép đổi chữ, còn `code` thì không.
Vượt hạn mức khi gọi API dữ liệu chứng khoán bằng Python thì bắt lỗi nào?
Với thư viện Python finlens, cả hai trường hợp đều nằm dưới `finlens.QuotaError`. Vượt giới hạn tần suất là `RateLimitError` (mã `FL_QUOTA_RATE`), mang thêm `.retry_after` là số giây nên chờ. Hết hạn mức trong ngày là `DailyQuotaExceededError` (mã `FL_QUOTA_DAILY`), mang `.resets_at` là thời điểm hạn mức được đặt lại kèm offset `+07:00` — chạy lại sau mốc đó, không sớm hơn. Trước khi cạn, thư viện phát `QuotaLowWarning` một lần cho mỗi client.
Một mã lỗi FL_ có tương ứng một lớp ngoại lệ riêng không?
Không, ánh xạ là nhiều-đối-một: nhiều mã có thể trỏ về cùng một lớp ngoại lệ của thư viện Python finlens. Ví dụ `FL_VALIDATION_INTERVAL` và `FL_VALIDATION_INTERVAL_AMBIGUOUS` cùng cho `InvalidIntervalError`. Vì vậy nên bắt theo lớp rồi phân biệt theo `.code`. Một mã không có trong bảng tra vẫn được ánh xạ về một lớp trong cây theo nhóm của nó, không bao giờ thành `RuntimeError` trần — đó là lý do `except finlens.FinLensError` vẫn đúng khi máy chủ thêm mã mới.
Gọi nhiều mã cùng lúc mà một mã hỏng thì chuyện gì xảy ra?
Tham số `on_error` quyết định, và mặc định là `"warn"`: thư viện Python finlens giữ phần lấy được, phát `PartialDataWarning`, rồi ghi chi tiết vào `df.attrs["finlens"]["partial"]` và `df.attrs["finlens"]["failed"]` (mã chứng khoán ánh xạ sang `code` và `message`). Đặt `on_error="raise"` thì nhận `PartialFetchError`, ngoại lệ này mang theo phần đã lấy được ở `.data` và ánh xạ mã sang ngoại lệ ở `.failures`, nên không phải gọi lại từ đầu. `on_error="ignore"` giữ dữ liệu và không cảnh báo gì.
Lỗi xác thực chứng chỉ TLS khi kết nối tới FinLens sửa thế nào?
Đó là `finlens.TlsVerificationError`, mã `FL_TRANSPORT_TLS`, thường gặp khi máy bạn nằm sau proxy kiểm tra TLS hoặc phần mềm diệt virus dùng CA riêng. Cách sửa là trỏ thư viện tới CA bundle do bộ phận IT cấp: `finlens.client(ca_bundle="/duong/dan/ca-bundle.pem")`, hoặc đặt biến môi trường `FINLENS_CA_BUNDLE`. Đây là lỗi vĩnh viễn nên thư viện không thử lại và nó cũng không làm mở circuit breaker. Lớp này kế thừa `ConnectionFailedError`, nên code đang bắt lớp cha không phải sửa gì.
Code cũ viết except ValueError quanh lời gọi finlens có còn chạy không?
Còn. `finlens.ValidationError` kế thừa cả `FinLensError` lẫn `ValueError`, và đó là chủ đích để code viết cho bản cũ không gãy khi nâng lên 1.x: các lỗi tham số như `InvalidSymbolError`, `InvalidDateRangeError`, `InvalidIntervalError`, `CompanyTypeMismatchError` và `UnitMismatchError` vẫn lọt vào `except ValueError`. Khi viết mới thì nên bắt `finlens.ValidationError` — nó chỉ bắt lỗi tham số của finlens chứ không nuốt luôn `ValueError` từ code khác của bạn.
Cập nhật lần cuối