From edc6b86f7273a33e3a18050fa7fd68fd1cf6dbd8 Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:58:58 -0700 Subject: [PATCH 1/2] fix: compute ISIN checksum instead of always accepting _isin_checksum never accumulated into check, so any 12-character ISIN-shaped string validated as True. Expand letters and apply Luhn. --- src/validators/finance.py | 39 ++++++++++++++++++++++----------------- tests/test_finance.py | 19 +++++++++++++++++-- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/validators/finance.py b/src/validators/finance.py index 9df5a970..170ad0cd 100644 --- a/src/validators/finance.py +++ b/src/validators/finance.py @@ -32,22 +32,27 @@ def _cusip_checksum(cusip: str): def _isin_checksum(value: str): - check, val = 0, None - - for idx in range(12): - c = value[idx] - if c >= "0" and c <= "9" and idx > 1: - val = ord(c) - ord("0") - elif c >= "A" and c <= "Z": - val = 10 + ord(c) - ord("A") - elif c >= "a" and c <= "z": - val = 10 + ord(c) - ord("a") + # Expand letters to two digits (A=10 .. Z=35), then apply Luhn from the right. + digits = [] + for idx, c in enumerate(value): + if "0" <= c <= "9": + if idx < 2: + return False + digits.append(ord(c) - ord("0")) + elif "A" <= c <= "Z": + n = 10 + ord(c) - ord("A") + digits.extend((n // 10, n % 10)) + elif "a" <= c <= "z": + n = 10 + ord(c) - ord("a") + digits.extend((n // 10, n % 10)) else: return False - if idx & 1: - val += val - + check = 0 + for i, d in enumerate(reversed(digits)): + if i & 1: + d *= 2 + check += d // 10 + d % 10 return (check % 10) == 0 @@ -82,10 +87,10 @@ def isin(value: str): [1]: https://en.wikipedia.org/wiki/International_Securities_Identification_Number Examples: - >>> isin('037833DP2') - ValidationError(func=isin, args={'value': '037833DP2'}) - >>> isin('037833DP3') - ValidationError(func=isin, args={'value': '037833DP3'}) + >>> isin('US0378331005') + True + >>> isin('US0378331006') + ValidationError(func=isin, args={'value': 'US0378331006'}) Args: value: ISIN string to validate. diff --git a/tests/test_finance.py b/tests/test_finance.py index a40fd333..c031f47a 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -24,13 +24,28 @@ def test_returns_failed_validation_on_invalid_cusip(value: str): # ==> ISIN <== # -@pytest.mark.parametrize("value", ["US0004026250", "JP000K0VF054", "US0378331005"]) +@pytest.mark.parametrize( + "value", + ["US0004026250", "US0378331005", "US5949181045", "GB0002634946"], +) def test_returns_true_on_valid_isin(value: str): """Test returns true on valid isin.""" assert isin(value) -@pytest.mark.parametrize("value", ["010378331005", "XCVF", "00^^^1234", "A000009"]) +@pytest.mark.parametrize( + "value", + [ + "010378331005", + "XCVF", + "00^^^1234", + "A000009", + # well-formed length/charset but wrong check digit (checksum was a no-op) + "US0378331006", + "JP000K0VF054", + "AAAAAAAAAAAA", + ], +) def test_returns_failed_validation_on_invalid_isin(value: str): """Test returns failed validation on invalid isin.""" assert isinstance(isin(value), ValidationError) From 445068906f934a4dd62416fed740b8e09614119d Mon Sep 17 00:00:00 2001 From: santhreal <64453045+santhreal@users.noreply.github.com> Date: Fri, 17 Jul 2026 22:09:40 -0700 Subject: [PATCH 2/2] fix: require ISIN check digit to be decimal per ISO 6166 --- src/validators/finance.py | 3 +++ tests/test_finance.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/validators/finance.py b/src/validators/finance.py index 170ad0cd..e713d966 100644 --- a/src/validators/finance.py +++ b/src/validators/finance.py @@ -33,6 +33,9 @@ def _cusip_checksum(cusip: str): def _isin_checksum(value: str): # Expand letters to two digits (A=10 .. Z=35), then apply Luhn from the right. + # ISO 6166: check digit (last char) must be decimal 0-9. + if len(value) != 12 or not value[-1].isdigit(): + return False digits = [] for idx, c in enumerate(value): if "0" <= c <= "9": diff --git a/tests/test_finance.py b/tests/test_finance.py index c031f47a..e0a9bfc3 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -44,6 +44,8 @@ def test_returns_true_on_valid_isin(value: str): "US0378331006", "JP000K0VF054", "AAAAAAAAAAAA", + # letter check digit can Luhn-pass; ISO 6166 requires a digit + "AA000000000G", ], ) def test_returns_failed_validation_on_invalid_isin(value: str):