How to Validate a VIN Check Digit in Python

Position 9 of a 17-character VIN is a mod-11 checksum defined in 49 CFR 565.15. Here is the full algorithm — transliteration table, weights, and the X — as tested Python code.

What the VIN check digit is

Every vehicle built since 1981 carries a 17-character Vehicle Identification Number. The characters are digits and letters, with three letters banned outright — I, O and Q — because they look like 1 and 0. The ninth character is special: for vehicles made for the North American market, US federal regulation 49 CFR 565.15 (implementing FMVSS 115, aligned with ISO 3779) requires it to be a check digit computed from the other sixteen characters. Rerun the computation on a VIN someone typed into your form and you catch most typos and many fabricated numbers instantly — no database call needed.

If you just want to check one number right now, paste it into our free VIN validator. If you want the general theory of why checksums catch typos, see how check digits work. The rest of this page builds the VIN-specific algorithm in Python.

The algorithm in three steps

  1. Transliterate: convert each of the 17 characters to a numeric value. Digits keep their face value; letters map through this table from 565.15:
A=1B=2C=3D=4E=5F=6G=7H=8
J=1K=2L=3M=4N=5P=7R=9
S=2T=3U=4V=5W=6X=7Y=8Z=9

Note the traps in that table: the second row skips 6 and 8 entirely (P is 7, R is 9), and the third row starts at S=2, not 1. Copying the values by memory instead of from the regulation is the single most common source of buggy VIN validators.

  1. Weight: multiply each value by its position weight: 8 7 6 5 4 3 2 10 0 9 8 7 6 5 4 3 2. Position 9 — the check digit itself — gets weight 0, so it never influences its own calculation.
  2. Mod 11: sum the 17 products and take the remainder mod 11. A remainder of 0–9 is the check digit as-is; a remainder of 10 is written as X. The VIN passes if this equals the character actually sitting in position 9.

Complete Python implementation

No dependencies beyond the standard library. Tested on Python 3.

import re

TRANSLITERATION = {
    "A": 1, "B": 2, "C": 3, "D": 4, "E": 5, "F": 6, "G": 7, "H": 8,
    "J": 1, "K": 2, "L": 3, "M": 4, "N": 5, "P": 7, "R": 9,
    "S": 2, "T": 3, "U": 4, "V": 5, "W": 6, "X": 7, "Y": 8, "Z": 9,
}
WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]

def vin_check_digit(vin: str) -> str:
    """Return the expected position-9 check digit ('0'-'9' or 'X')."""
    vin = re.sub(r"[^A-Za-z0-9]", "", vin).upper()
    if len(vin) != 17:
        raise ValueError(f"VIN must be 17 characters, got {len(vin)}")
    if re.search(r"[IOQ]", vin):
        raise ValueError("VIN may not contain the letters I, O, or Q")
    total = 0
    for i, ch in enumerate(vin):
        value = int(ch) if ch.isdigit() else TRANSLITERATION[ch]
        total += value * WEIGHTS[i]
    remainder = total % 11
    return "X" if remainder == 10 else str(remainder)

def validate_vin(vin: str) -> bool:
    """True if the VIN's 9th character matches its computed check digit."""
    cleaned = re.sub(r"[^A-Za-z0-9]", "", vin).upper()
    return vin_check_digit(cleaned) == cleaned[8]

print(validate_vin("1M8GDM9AXKP042788"))    # True
print(vin_check_digit("1HGCM82633A004352")) # 3  (matches position 9)
print(validate_vin("11111111111111111"))    # True (classic test vector)

Walking through it: the regex strips spaces and hyphens (VINs are often pasted as 1M8-GDM9AX-KP042788) and .upper() normalizes lowercase input. The two hard rejections — wrong length and I/O/Q — come before any arithmetic, because no genuine VIN anywhere in the world violates them. Then each character is transliterated, multiplied by its weight, summed, and reduced mod 11. The comparison at the end happens between strings, which is what makes the X case work with no special handling.

Worked example: 1M8GDM9AXKP042788

This is a known-valid VIN whose check digit happens to be the interesting one, X. Position by position:

Pos1234567891011121314151617
Char1M8GDM9AXKP042788
Value14874491727042788
Weight876543210098765432
Product8284835161218100185602410282416

Sum of products: 8+28+48+35+16+12+18+10+0+18+56+0+24+10+28+24+16 = 351. Then 351 mod 11 = 10 (since 11 × 31 = 341), and a remainder of 10 is written as X. Position 9 of the VIN is indeed X, so validate_vin returns True. Note how the X in position 9 was transliterated to 7 in the value row but then multiplied by weight 0 — the check digit participates in the loop yet contributes nothing, which is exactly what the standard intends.

Edge cases and classic mistakes

A failing VIN is not necessarily a fake VIN

Here is the honest limitation, and it is bigger for VINs than for most identifiers. The check digit is mandatory only for vehicles built for the North American market. ISO 3779 lists it as optional, and most European and many Asian manufacturers simply use position 9 as an ordinary attribute character. A genuine Fiat or BMW VIN can fail this test while a carefully forged one passes it. So treat the result asymmetrically: for a US- or Canada-market vehicle a failure is a strong signal of a typo or tampering; for a European VIN it proves nothing by itself. And in either case a pass only means internal consistency — it does not prove the VIN was ever assigned to a real vehicle. For that you need a registry lookup (for example NHTSA's free vPIC decoder for US vehicles). Our VIN validator applies the structural rules plus the checksum and flags this regional caveat explicitly.

Validating at scale?

If VINs arrive by the thousands — dealer feeds, insurance intakes, fleet imports — the same algorithm is available as a JSON API that accepts up to 100 VINs per call:

curl -X POST https://codeclassify-api.rosariovitale0096.workers.dev/v1/vin/validate \
  -H "X-Api-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"vins":["1M8GDM9AXKP042788","1HGCM82633A004352"]}'

Response:

{"ok":true,"count":2,"valid":2,
 "standard":"ISO 3779 / FMVSS 115 (check digit)",
 "results":[
  {"input":"1M8GDM9AXKP042788","vin":"1M8GDM9AXKP042788","valid":true,"expected_check_digit":"X"},
  {"input":"1HGCM82633A004352","vin":"1HGCM82633A004352","valid":true,"expected_check_digit":"3"}]}

Invalid entries come back with a reason (wrong_length, contains_IOQ, bad_format) or the expected_check_digit they should have had. The free tier includes 10 calls per month — grab a key on the API page. For validating a whole spreadsheet in one go, see the CSV bulk-validation guide.

FAQ

Why can't a VIN contain the letters I, O, or Q?

ISO 3779 and 49 CFR 565 exclude I, O and Q from every position of a VIN because they are too easy to confuse with the digits 1 and 0 on stamped plates and handwritten paperwork. This rule applies worldwide, so a 17-character string containing any of those three letters is not a valid VIN anywhere — you can reject it before even computing the checksum.

My VIN fails the check digit — is the vehicle fake?

Not necessarily. The position-9 check digit is mandatory for vehicles built for the North American market under 49 CFR 565 (and Canada's equivalent rules). ISO 3779, the international standard, treats it as optional, so many European- and Asian-market VINs are genuine yet fail the mod-11 test. A failure is a strong red flag for a US or Canadian vehicle but inconclusive for one sold elsewhere. To confirm a VIN actually belongs to a real vehicle you need a registry decoder, not a checksum.

What does an X in position 9 of a VIN mean?

The VIN checksum is computed mod 11, so the remainder can be anything from 0 to 10. Because 10 does not fit in one character, the standard writes it as the letter X. An X in position 9 is therefore perfectly normal — roughly one VIN in eleven has it. Remember to compare it as a string in code: converting the check digit to an integer will crash on X.

Check a VIN without writing code

Paste any 17-character VIN into the free VIN validator — it runs the length, character and mod-11 checks above and tells you the expected check digit when one doesn't match.

This guide is for general information only. A passing check digit confirms a VIN is internally consistent, not that it is assigned to a real vehicle, and non-North-American VINs may be genuine without a valid check digit. For vehicle history or title decisions, consult an official registry or decoder such as NHTSA vPIC.