#2465·faker

en_GB.vat_id() returns a number the UK validator accepts only 27% of the time

Author: feiiiiii5Created Sep 27, 2026Updated Sep 27, 2026

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:

python
# 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:

python
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):

  1. For "GB### #### ##" and the 12-digit form, draw the body and compute the check digits instead of drawing them.
  2. For "GBGD###" draw from 000-499, and for "GBHA###" from 500-999 — or pick the prefix from the range that matches the digits.
  3. 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.