How to Validate GS1 GLN Check Digits in Python
The bug is specific and it hides in plain sight: a Global Location Number arrives on a supplier feed with two adjacent digits swapped, the ingestion code confirms it is thirteen characters of digits, and the transposed value is written into a Key Data Element as a trusted location_id. The length check passed. The type check passed. Nothing rejected it. Only weeks later, during a recall traceback, does the identifier resolve to no known facility — and by then the corrupted GLN sits inside a committed Critical Tracking Event, breaking the ship-from/receive-to link the trace depends on. This guide implements and unit-tests the fix: the GS1 modulo-10 check-digit validator that catches a transposed GLN the moment it arrives, as part of the broader GS1 Identifier Validation gate.
The failure is worth naming precisely because it is a failure of validating the wrong property. A GLN has thirteen digits, but not every thirteen-digit number is a valid GLN. The thirteenth digit is a check digit computed from the first twelve, and that computation is exactly what makes a transposition detectable — swapping two digits almost always changes the weighted sum and therefore breaks the check digit, even though it never changes the length.
Root Cause Analysis
The GS1 check digit exists to catch the two most common human and transmission errors: a single mistyped digit and a transposition of two adjacent digits. It does this with a positional weighting scheme. Reading the twelve payload digits from right to left, alternate digits are weighted by 3 and 1, the weighted values are summed, and the check digit is the number that raises that sum to the next multiple of ten. Because adjacent positions carry different weights (one gets 3, its neighbor gets 1), swapping two adjacent digits changes the weighted sum by 2 times their difference — a nonzero amount unless the digits are equal — so the check digit no longer matches.
A validator that checks only len(gln) == 13 and gln.isdigit() never computes that sum, so it is blind to the entire class of errors the check digit was designed to catch. This is why the defect is silent in an FSMA 204 pipeline: the transposed GLN is structurally plausible at the level the naive check inspects, and it flows through normalization and into a KDE looking exactly like a valid location identifier. The parent Schema Validation Rules gate confirms the field is a well-formed string, but confirming the check digit is a separate arithmetic step that must run before the value is trusted.
Figure — modulo-10 weighting on a 13-digit GLN:
Minimal Reproducible Example
The snippet below shows the exact defect. A supplier sends a GLN whose middle digits were transposed during manual entry; the naive validator accepts it because it inspects only length and digit-composition, and the corrupted identifier is bound to a KDE.
# Known-good GLN for a distribution center: 0300123456785
# A keying error transposed the adjacent 4 and 5 mid-string, keeping length at 13.
good_gln = "0300123456785"
transposed_gln = "0300123546785" # the 4 and 5 in the payload were swapped
def naive_is_gln(value: str) -> bool:
# The bug: validates the wrong property. Length and numeric only.
return len(value) == 13 and value.isdigit()
print(naive_is_gln(good_gln)) # True (correct)
print(naive_is_gln(transposed_gln)) # True (WRONG - this GLN is corrupt)
Both calls return True. The transposed GLN sails through, and downstream the pipeline treats 0641141000058 as a legitimate location. There is no exception, no log line, no quarantine entry — the record commits and the defect becomes invisible until a trace query cannot find the location.
Fix Implementation
The fix computes the check digit from the first twelve digits and compares it to the thirteenth. The weighting is the crux: reading the payload from the right, the rightmost payload digit gets weight 3, the next gets 1, and so on, alternating. Summing the weighted products and closing to the next multiple of ten yields the expected check digit.
from __future__ import annotations
import logging
gln_logger = logging.getLogger("fsma204.gln")
def gln_check_digit(payload_12: str) -> int:
"""Return the GS1 mod-10 check digit for the 12 payload digits of a GLN.
Reading right to left, the rightmost payload digit is weighted 3, the next 1,
alternating. The check digit closes the weighted sum to the next multiple of ten.
"""
total = 0
for position, char in enumerate(reversed(payload_12)):
weight = 3 if position % 2 == 0 else 1
total += int(char) * weight
return (10 - (total % 10)) % 10
def is_valid_gln(value: str) -> bool:
"""Validate a 13-digit GLN: correct length, all numeric, closing check digit."""
if len(value) != 13 or not value.isdigit():
return False
return gln_check_digit(value[:-1]) == int(value[-1])
def validate_gln_or_quarantine(value: str) -> str:
"""Return the GLN if valid; otherwise log and raise for quarantine routing."""
if not is_valid_gln(value):
gln_logger.warning("GLN_REJECTED | value=%s | reason=check_digit_or_format", value)
raise ValueError(f"Invalid GLN: {value}")
gln_logger.info("GLN_ACCEPTED | value=%s", value)
return value
Note the framing difference from the shared batch validator described in the GS1 Identifier Validation guide: here gln_check_digit operates on the twelve payload digits directly and returns the digit itself, which is the form you want when unit-testing the arithmetic in isolation. The validate_gln_or_quarantine wrapper is what an ingestion path calls, raising on failure so the caller can route the value to the same quarantine store the Schema Validation Rules gate uses.
Verification Steps
Verification hinges on testing both directions: a known-good GLN must pass, and a deliberately corrupted one must fail. The transposition case is the acceptance test that actually proves the fix, because it is exactly what the naive validator missed.
import pytest
def test_known_good_gln_passes():
assert is_valid_gln("0300123456785") is True
def test_transposed_gln_fails():
# The MRE case: length-valid but check-digit-invalid.
assert is_valid_gln("0300123546785") is False
def test_single_digit_typo_fails():
# Flip the check digit itself: 5 -> 6.
assert is_valid_gln("0300123456786") is False
def test_wrong_length_fails():
assert is_valid_gln("030012345678") is False
def test_non_numeric_fails():
assert is_valid_gln("030012345678X") is False
def test_check_digit_arithmetic():
# Weighted sum of the payload is 85; the check digit closes it to 90.
assert gln_check_digit("030012345678") == 5
def test_quarantine_raises_on_bad_gln():
with pytest.raises(ValueError):
validate_gln_or_quarantine("0300123546785")
Running pytest on this module confirms all seven assertions. In production, watch the structured log: an accepted GLN emits GLN_ACCEPTED | value=... at INFO, and a rejection emits GLN_REJECTED | value=... | reason=check_digit_or_format at WARNING. Alerting on the rejection rate per supplier surfaces the upstream keying or mapping problem that produced the bad identifiers, feeding the Data Quality Monitoring layer rather than letting the count drift.
Related Edge Cases
Three neighboring cases deserve a test of their own once the GLN validator is in place. First, a GLN transmitted as an integer rather than a string loses any leading zero — 0614141000058 becomes 614141000058, twelve digits — so the length check must run against the string form before any numeric coercion, and the ingestion path should reject numeric-typed identifiers outright. Second, a GLN embedded in a longer GS1 element string (for example, prefixed with an application identifier) must be extracted to its bare thirteen digits before validation, or the length check fails on a structurally fine key. Third, the same modulo-10 arithmetic extends to the fourteen-digit GTIN-14 and eighteen-digit SSCC, but the payload length and leading-digit rules differ; the Validate GTIN & SSCC guide covers those variants. New suppliers whose feeds trigger these edge cases are registered through Supplier Onboarding Automation.
Frequently Asked Questions
Why does a transposed GLN pass a length check but fail the check digit?
A transposition swaps two adjacent digits without adding or removing any, so the length stays at thirteen. The check digit, however, weights adjacent positions differently — one by 3 and its neighbor by 1 — so swapping them changes the weighted sum by twice the difference of the two digits, which is nonzero unless the digits are equal. That changed sum no longer closes to the transmitted check digit, so the validator rejects it.
Which digits does the weight of 3 apply to?
Reading the twelve payload digits from right to left, the rightmost payload digit gets weight 3, the next gets 1, and the pattern alternates. Equivalently, on a full thirteen-digit GLN the even-numbered positions counting from the left carry weight 3. The code avoids ambiguity by iterating over the reversed payload and applying 3 to position zero.
What known-good GLN can I use as a test fixture?
The value 0300123456785 is a well-formed GLN whose payload weighted sum is 85 and whose check digit is 5, making it a reliable positive fixture. Pair it with a transposed variant such as 0300123546785 as the negative fixture, since that value keeps the length valid while breaking the check digit. Testing both directions is what proves the validator catches the class of error the naive check misses.
Should the validator handle a GLN sent as a number rather than a string?
It should reject it rather than coerce it. A GLN transmitted as an integer loses any leading zero, so 0614141000058 arrives as twelve digits and would fail the length check for the wrong reason. Keeping the identifier as a string end to end, and rejecting numeric-typed values at the boundary, preserves leading zeros and keeps the failure diagnosis accurate.
Where does a rejected GLN go?
It is routed to the same durable quarantine store the schema validation gate uses, carrying the original value and the failure reason, and never silently dropped. The validate_gln_or_quarantine wrapper raises on failure so the ingestion caller can write the quarantine artifact and continue with the rest of the batch. This preserves the exact bytes the supplier sent for later reconciliation.
Related
- GS1 Identifier Validation — the parent guide covering all three GS1 keys and the shared mod-10 function.
- Validate GTIN & SSCC — extends this arithmetic to 14- and 18-digit identifiers.
- Schema Validation Rules — the KDE contract that trusts a validated GLN as a location_id.
- KDE Field Mapping Guide — maps the GLN to the ship-from and receive-to location KDEs.
- Supplier Onboarding Automation — registers supplier GLNs and prefixes before the first live batch.
Up: GS1 Identifier Validation — this guide is the GLN-specific implementation of the parent validation gate.