en_GB.vat_id() returns a number the UK validator accepts only 27% of the time
en_GB.vat_id() returns a number the UK validator accepts only 27% of the time
vat_id() picks one of four patterns and fills every # at random:
# faker/providers/ssn/en_GB/__init__.py:27
vat_id_formats: Tuple[str, ...] = (
"GB### #### ##",
"GB### #### ## ###",
"GBGD###",
"GBHA###",
)Two things are not modelled: the check digits of the numeric forms, and the numeric range
that the GD/HA government forms are allowed to use. Validated against
stdnum.gb.vat, 6000 samples at
Faker.seed(20260927):
| format | accepted | |
|---|---|---|
GB### #### ## |
48/1512 — 3.2% | the two check digits are random |
GB### #### ## ### |
37/1483 — 2.5% | same, plus the branch-identifier rules |
GBGD### |
754/1495 — 50.4% | GD is only valid below 500 |
GBHA### |
755/1510 — 50.0% | HA is only valid from 500 up |
| all | 1594/6000 — 26.6% |
The two causes, checked separately
The numeric check digits are random. Holding the 7-digit body fixed and trying all 100 two-digit suffixes, a real validator accepts only a few — and not always exactly one:
body=0668692 valid suffixes: ['21']
body=1234567 valid suffixes: ['27', '40', '82']
body=9999999 valid suffixes: ['18', '31', '73']So a uniformly random suffix lands on a valid one 1–3% of the time depending on the body, which is what the table shows.
GD and HA ignore their range. Enumerating all 1000 three-digit values for each
prefix:
GBGD000-999 accepted: 500 (the rule allows 000-499)
GBHA000-999 accepted: 500 (the rule allows 500-999)Exactly 500 each, in the direction the rule predicts. Drawing the three digits uniformly from 000-999 therefore produces a valid number half the time, no matter how good the rest is.
Why the test suite does not catch it
tests/providers/test_ssn.py:237 asserts the shape and nothing else:
def test_vat_id(self):
for _ in range(100):
assert re.search(
r"^GB\d{3} \d{4} \d{2}(?: \d{3})?$|^GB(?:GD|HA)\d{3}$",
self.fake.vat_id(),
)A regex over the format string cannot see a wrong check digit or an out-of-range GD/HA
value — both keep the string shape identical.
The promise
vat_id()'s own docstring says it returns "A random British VAT ID" and cites the EU VIES
FAQ (.../vies/faq.html#item_11) as its source. VIES is the service that validates UK VAT
numbers, so a number VIES rejects is not a British VAT ID in the sense the docstring is
using, and roughly three in four of these are rejected.
What a fix looks like
Small and self-contained, in the same shape as the Norwegian provider that already does
this (faker/providers/bank/no_NO computes its MOD11 digit rather than drawing it):
- For
"GB### #### ##"and the 12-digit form, draw the body and compute the check digits instead of drawing them. - For
"GBGD###"draw from 000-499, and for"GBHA###"from 500-999 — or pick the prefix from the range that matches the digits. - Extend the test to validate a generated value, not just its shape.
I checked the open issues and open PRs and did not find this reported; #864 is about
vat_id() not existing on a Generator, which is a different problem and is closed.
Provenance: main at 975dd2a1 and released faker==40.39.0, both reproducing the values
above at the seed given. Oracle: python-stdnum 2.2.
Source: joke2k/faker