How to Validate a GTIN Check Digit in Python

One short function handles GTIN-8, UPC-A, EAN-13 and GTIN-14 — if you clean the input properly and never, ever store barcodes as integers.

What we are validating

A GTIN (Global Trade Item Number) is the number under a retail barcode. It comes in four lengths — GTIN-8, GTIN-12 (UPC-A), GTIN-13 (EAN-13) and GTIN-14 for shipping cartons — and in every one of them the last digit is a checksum computed from all the others with the GS1 mod-10 algorithm: weight the digits alternately 3 and 1 starting from the right, sum the products, and pick the digit that lifts the total to the next multiple of 10. If you want the theory and error-detection math, read how check digits work; if you just need one code checked right now, the GTIN check digit calculator does it in the browser. This guide is for the third case: you have Python and a pile of codes.

The implementation

Save this as gtin.py. The examples in the docstrings are real doctests — every one of them was executed before this article was published (Python 3.14, zero failures).

VALID_LENGTHS = {8, 12, 13, 14}


def clean_gtin(raw):
    """Normalize pasted or scanned input to bare digits.

    Removes spaces and hyphens (common in formatted codes) but refuses
    to guess at anything else: a letter O where a zero should be is a
    data problem you want to see, not silently delete.

    >>> clean_gtin(" 0 36000 29145 2 ")
    '036000291452'
    >>> clean_gtin("4-006381-333931")
    '4006381333931'
    >>> clean_gtin("03600O291452")
    Traceback (most recent call last):
        ...
    ValueError: non-digit character in GTIN: '03600O291452'
    """
    s = str(raw).replace(" ", "").replace("-", "")
    if not (s.isascii() and s.isdigit()):
        raise ValueError(f"non-digit character in GTIN: {raw!r}")
    return s


def gtin_check_digit(body):
    """Return the GS1 mod-10 check digit for a GTIN body (the code
    without its final digit).

    >>> gtin_check_digit("03600029145")   # 11-digit UPC-A body
    2
    >>> gtin_check_digit("400638133393")  # 12-digit EAN-13 body
    1
    >>> gtin_check_digit("9638507")       # 7-digit GTIN-8 body
    4
    """
    digits = clean_gtin(body)
    total = sum(int(d) * (3 if i % 2 == 0 else 1)
                for i, d in enumerate(reversed(digits)))
    return (10 - total % 10) % 10


def is_valid_gtin(code):
    """True if code is a complete GTIN-8/12/13/14 whose last digit
    matches the computed check digit.

    >>> is_valid_gtin("036000291452")   # UPC-A
    True
    >>> is_valid_gtin("4006381333931")  # EAN-13
    True
    >>> is_valid_gtin("96385074")       # GTIN-8
    True
    >>> is_valid_gtin("036000291453")   # last digit wrong
    False
    >>> is_valid_gtin(36000291452)      # leading zero lost: 11 digits
    False
    """
    try:
        digits = clean_gtin(code)
    except ValueError:
        return False
    if len(digits) not in VALID_LENGTHS:
        return False
    return gtin_check_digit(digits[:-1]) == int(digits[-1])

Three deliberate design choices:

Run the doctests with python -m doctest gtin.py — silence means all eleven pass.

Worked example: 03600029145 → 2

Take the classic 11-digit UPC-A body 03600029145. Walking from the right and alternating weights 3, 1, 3, 1…

5×3 + 4×1 + 1×3 + 9×1 + 2×3 + 0×1 + 0×3 + 0×1 + 6×3 + 3×1 + 0×3 = 15 + 4 + 3 + 9 + 6 + 0 + 0 + 0 + 18 + 3 + 0 = 58.

The next multiple of 10 is 60, so the check digit is 60 − 58 = 2 and the full code is 036000291452 — exactly what gtin_check_digit("03600029145") returned in the doctest above. The same function run on the GTIN-14 body 0001234567890 gives 5, so is_valid_gtin("00012345678905") is True: fourteen digits, three leading zeros, same math.

Batch-validating a list

With is_valid_gtin guaranteed not to raise, batch checking is a one-liner:

codes = [
    "036000291452",      # valid UPC-A
    "4006381333931",     # valid EAN-13
    "0 36000 29145 2",   # formatted, still valid after cleaning
    "036000291453",      # wrong check digit
    "12345",             # wrong length
]

invalid = [c for c in codes if not is_valid_gtin(c)]
print(invalid)
# ['036000291453', '12345']

Note that the space-formatted code passes: clean_gtin strips the presentation characters before the length check. If your codes live in a spreadsheet rather than a list, the browser-based bulk barcode validator does the same pass over pasted columns without any code.

Edge cases that actually bite

Leading zeros are the big one. Every UPC-A that starts with 0 — and that is a lot of them — is destroyed the moment it becomes a number. You cannot even type it: 036000291452 as an int literal is a SyntaxError in Python 3. More insidiously, a CSV opened in Excel or read with a numeric dtype hands you 36000291452, an 11-digit value that fails the length check — which is why the doctest deliberately feeds the function an int and expects False. Read barcode columns as text (dtype=str in pandas, csv module strings otherwise), and see fixing leading zeros in Excel if the damage already happened upstream.

Clean narrowly, reject loudly. The cleaner strips only spaces and hyphens, because those are formatting. It does not coerce the letter O to zero or drop stray characters: those are data-entry errors, and silently “fixing” them can turn an invalid code into a valid-looking wrong one. Related: plain str.isdigit() alone is too generous — it accepts superscripts and non-ASCII numerals that int() may reject — hence the paired s.isascii() and s.isdigit() test.

Wrong algorithm, plausible results. The GS1 scheme is often confused with the Luhn algorithm used for card numbers (try the Luhn checker to see the difference). Both are mod-10, but Luhn doubles digits and folds the products; a Luhn library will happily reject valid GTINs. And an 11- or 15-digit input is not a GTIN at all — 11 digits almost always means a lost leading zero, not a new format. If you need to move between UPC-A, EAN-13 and GTIN-14 representations, that is a conversion problem, not a validation one: the UPC to EAN converter recomputes the check digit as part of the conversion.

Checksum is not registry

Everything above answers one question: is this number internally consistent? A True from is_valid_gtin does not mean the GTIN was ever licensed by GS1 to a company or assigned to a product — 96385074 passes the math and is a documentation example, not something on a shelf. Checksum validation is the fast, free filter you run first; registry verification is a separate, slower step. Our methodology page spells out exactly which checks each tool performs.

Validating at scale?

If the codes arrive by the thousands — supplier feeds, marketplace listings, ERP imports — the same deterministic check is available as a JSON API that accepts up to 100 codes per call:

curl -X POST "https://codeclassify-api.rosariovitale0096.workers.dev/v1/gtin/validate" \
  -H "X-Api-Key: ccl_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"codes":["036000291452","036000291453"]}'
{"ok":true,"count":2,"valid":1,"results":[
  {"input":"036000291452","code":"036000291452","format":"UPC-A","valid":true},
  {"input":"036000291453","code":"036000291453","format":"UPC-A","valid":false,
   "expected_check_digit":2,"corrected":"036000291452"}
]}

Invalid codes come back with the expected check digit and the corrected code, so a repair pass is one field away. The free tier is 10 calls per month with no card required — details and signup on the API page, and there is a full walkthrough in bulk-validating a CSV with the API.

FAQ

Why must GTINs be stored as strings and not integers in Python?

Because GTINs can start with zero and integers cannot. Typing 036000291452 as an int literal is a SyntaxError in Python 3, and a value that arrives as the number 36000291452 has already lost its leading zero — it is 11 digits long, which is not a valid GTIN length. Keep codes as strings end to end: read CSV columns as text, quote the values in spreadsheets, and only convert individual digits to int inside the checksum math.

Can one Python function validate GTIN-8, UPC-A, EAN-13 and GTIN-14?

Yes. The GS1 algorithm assigns weights starting from the rightmost digit, and the digit just left of the check digit always gets weight 3. Because the weighting is right-aligned, the same code works for all four lengths — the function only needs to confirm that the cleaned input is 8, 12, 13 or 14 digits long before running the math.

Does a valid check digit prove the barcode is registered with GS1?

No. The check digit only proves the number is internally consistent — that it was not obviously mistyped or misread. Whether that GTIN has actually been licensed to a company and assigned to a real product is a registry question, which a checksum cannot answer. Treat checksum validation as a cheap first filter that runs before slower registry lookups.

Check a GTIN without opening a terminal

Paste any UPC, EAN or GTIN-14 into the GTIN check digit calculator to compute or verify the final digit instantly — same algorithm, zero setup.

This guide is for general information only. Check digit validation confirms the internal consistency of a number, not that a GTIN is officially licensed, assigned, or in commercial use. For barcode assignment and licensing, consult GS1 or your local GS1 member organization.