no_NO.iban() is invalid when the account starts 0000: Postgiro uses Luhn
no_NO.iban() is invalid whenever the account number starts with 0000
A Norwegian account number is 11 digits: a 4-digit bank register number, six more
digits, and a MOD11 check digit. The provider always applies MOD11 — and MOD11 is
correct here. The problem is the register number: 0000 is not a bank register
number, it is the Postgiro bank code, and a Postgiro account is a 7-digit number
whose check digit is modulus 10 (Luhn). So when the random register number lands
on 0000, the check digit is the wrong kind, and about 1 generated IBAN in 11,000
is invalid.
from faker import Faker
from stdnum.iban import validate
fake = Faker("no_NO")
Faker.seed(0)
for i in range(1, 2000):
iban = fake.iban()
try:
validate(iban)
except Exception as exc:
print(i, iban, type(exc).__name__)
break1864 NO7900005649935 InvalidChecksumReading that value: the register number is 0000, and the remaining seven digits
5649935 are a Postgiro account number, whose check digit must satisfy Luhn. It
does not, so validate raises InvalidChecksum.
MOD11 itself is right, so this is only the 0000 case
Worth saying explicitly, since the obvious suspicion is that the weights are
wrong. They are not. faker/providers/bank/no_NO/__init__.py uses weights
(6, 7, 8, 9, 4, 5, 6, 7, 8, 9) and takes sum % 11; the published standard
states the same computation as weights 5432765432 with check digit
11 - (sum % 11). Since each of those is 11 - w, the two are the same number
mod 11 — confirmed against the standard's own worked example (1234.45.6845 →
check digit 4) and against stdnum.no.kontonr, which uses faker's form. Across
200,000 samples faker's check digit was always what MOD11 requires.
Why the test suite does not catch it
TestNoNo already has the right test — tests/providers/test_bank.py:456:
def test_iban_stdnum(self, faker, num_samples):
for _ in range(num_samples):
iban_validator.validate(faker.iban())It is just under-powered for this defect. num_samples defaults to 100
(tests/providers/conftest.py:36-41), so a run hits a 0000 register number
about 1% of the time — CI passes on this bug roughly 99% of runs. The
hand-written is_valid_iban used by test_iban only checks mod-97 and cannot
see it at all. The promise being broken is the bank provider's own, at
faker/providers/bank/__init__.py:17-22: values "conform to some standard/format,
are of the expected lengths, and have valid checksums (where applicable)".
What the check digit should be
stdnum.no.kontonr.compact strips a leading 0000 — commented "strip leading
0000 postgiro bank code" — and validate then branches on length: 7 digits go
through luhn.validate, 11 digits through the MOD11 check. That module was
written from the Perl GAAS::Norge::No::KontoNr, whose logic is quoted in
python-stdnum#79, so
the rule is not one implementation's private convention.
Two ways to fix it; I have no preference
- Apply Luhn when the register number is
0000, keeping Postgiro numbers valid. This is whatstdnumexpects, and it makes the existingtest_iban_stdnumproof rather than 1%-likely proof. - Never generate
0000as the register number. The first four digits are the bank's register number and0000is the Postgiro code, so a bank provider arguably should not emit it. Simpler, at the cost of that number space.
Either way the test wants a seeded Postgiro case rather than 100 random draws.
Full 200,000-sample measurementFaker.seed(20260927), every value through stdnum.iban.validate
BBANs starting 0000: 17 rejected: 16
all rejections Postgiro-prefixed: True
all rejections Luhn-invalid after stripping: 16/16
observed 1 in 12,500; expected 1 in ~11,100 (P(0000)=1/10,000 x P(Luhn fails)~0.9)The 17-vs-16 gap is the one Postgiro number that happened to satisfy Luhn too.
Rejected values include NO4200008890498, NO5400007024487, NO2900007415362,
NO2900000693650, NO5300005114187, NO9200007190352.
The same standard restricts digits 5 and 6 (the "account group"), which an
unconstrained ########### cannot respect: 00 is "reserved for settlement
accounts between banks", and 90-99 is "reserved for the banks' internal
purposes". So ~10% of generated numbers land in 90-99 and ~1% in 00. I have
no second implementation that checks this, so I am not claiming it as a defect —
it may be out of scope, or fine for generated test data. Flagging it because it
surfaced in the same investigation.
Provenance: main at 975dd2a1 and released faker==40.39.0, both reproducing
the values above at the seeds given. Norwegian rules: Standard for Account Number
in the Norwegian banking community, v10.
Source: joke2k/faker