EAN Barcode Validation in Python — GS1 Checksum and Prefix Lookup
EAN barcodes are the backbone of global retail — every product on every shelf has one. Here's how the checksum works, what the prefix encodes, and how to validate barcodes reliably in your Python application.
In this guide
- 1. What is an EAN barcode?
- 2. EAN-13 vs EAN-8 — when each is used
- 3. GS1 checksum — alternating weights 1 and 3
- 4. The GS1 prefix — what it actually means
- 5. Special prefixes — ISBN, ISSN, and coupons
- 6. The production-ready solution
- 7. Python code example
- 8. cURL example
- 9. Understanding the response
- 10. Edge cases
1. What is an EAN barcode?
EAN stands for European Article Number, now officially called GTIN (Global Trade Item Number) by GS1 — the standards body that manages product identification globally. Despite the rebrand, "EAN" and "barcode" remain the common terms in practice.
Every EAN encodes a product identifier as a sequence of digits with a trailing check digit. When a barcode scanner reads the stripes, it decodes the digits and validates the checksum — ensuring the scan was not corrupted by a dirty or damaged label.
In e-commerce and inventory systems, EAN codes are used to identify products across suppliers and marketplaces, match catalog entries, and feed product databases.
2. EAN-13 vs EAN-8 — when each is used
EAN-13 (13 digits)
The standard for retail products worldwide. Encodes GS1 prefix (2–3 digits), company code, product code, and check digit.
EAN-8 (8 digits)
Used for small items where a full 13-digit barcode would not fit — cigarette packs, pencils, chewing gum. Assigned only by GS1 on request.
3. GS1 checksum — alternating weights 1 and 3
The checksum algorithm is simpler than Luhn — instead of doubling, it alternates fixed weights of 1 and 3, then computes the complement to 10.
Let's walk through 5901234123457:
Step 1 — Apply alternating weights (1 and 3) to the first 12 digits
| Digit | 5 | 9 | 0 | 1 | 2 | 3 | 4 | 1 | 2 | 3 | 4 | 5 | 7 |
| Weight | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | 1 | 3 | — |
| Product | 5 | 27 | 0 | 3 | 2 | 9 | 4 | 3 | 2 | 9 | 4 | 15 | ? |
Step 2 — Sum the products
5 + 27 + 0 + 3 + 2 + 9 + 4 + 3 + 2 + 9 + 4 + 15 = 83
Step 3 — Check digit = (10 − sum mod 10) mod 10
(10 − (83 % 10)) % 10 = (10 − 3) % 10 = 7
Last digit of barcode is 7 ✓ — valid EAN-13
# ean_checksum.py — validate EAN-8 or EAN-13 import re def validate_ean(raw: str) -> bool: digits = re.sub(r"[\s-]", "", raw) if not digits.isdigit(): return False if len(digits) not in (8, 13): return False # EAN-13: weights 1,3,1,3,... — EAN-8: weights 3,1,3,1,... weights = ( [1, 3] * 6 if len(digits) == 13 else [3, 1] * 4 ) total = sum( int(d) * w for d, w in zip(digits[:-1], weights) ) check_digit = (10 - (total % 10)) % 10 return check_digit == int(digits[-1]) print(validate_ean("5901234123457")) # True ✓ EAN-13 print(validate_ean("96385074")) # True ✓ EAN-8 print(validate_ean("5901234123458")) # False ✗ bad check digit
4. The GS1 prefix — what it actually means
The first 2–3 digits of an EAN-13 are the GS1 prefix, assigned to a GS1 Member Organisation by country. A common misconception is that it indicates the country of origin of the product — it does not. It indicates which GS1 national organisation issued the company number.
590 (Poland) can be manufactured anywhere in the world — it only means the company registered its barcodes through GS1 Poland. Prefix is not country of origin.| Prefix | GS1 Member Organisation |
|---|---|
| 00–09 | United States & Canada |
| 30–37 | France |
| 40–44 | Germany |
| 45, 49 | Japan |
| 50 | United Kingdom |
| 57 | Denmark |
| 590 | Poland |
| 690–695 | China |
| 73 | Sweden |
| 76 | Switzerland |
| 80–83 | Italy |
| 84 | Spain |
| 87 | Netherlands |
| 978–979 | Bookland (ISBN) |
5. Special prefixes — ISBN, ISSN, and coupons
978–979 — Bookland (ISBN)
EAN-13 barcodes on books use prefix 978 or 979, encoding the ISBN-13. The digits after the prefix correspond to the book's ISBN (minus the check digit, which is recalculated for EAN). An ISBN-13 and its EAN-13 barcode are the same number.
# Check if an EAN-13 encodes a book (ISBN) def is_book_ean(ean13: str) -> bool: return ean13.startswith("978") or ean13.startswith("979")
977 — ISSN (Serials / Magazines)
Periodicals (magazines, newspapers, journals) use prefix 977, encoding the ISSN. The 8-digit ISSN is embedded in digits 4–10 of the EAN-13.
20–29 — In-store restricted codes
Prefix 20–29 is reserved for in-store use — price-embedded barcodes on weighed goods (deli, fresh produce), store-specific loyalty items, or internal inventory labels. These codes are not globally unique and should not be used in external product databases.
6. The production-ready solution
The IsValid EAN API validates the format (8 or 13 digits), computes the GS1 checksum, and for EAN-13 returns the GS1 prefix and the associated member organisation country.
Full parameter reference and response schema: EAN Validation API docs →
7. Python code example
Using the isvalid-sdk package or the requests library. Install with pip install isvalid-sdk or pip install requests.
# ean_validator.py import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) result = iv.ean("5901234123457") if not result["valid"]: print("Invalid EAN barcode") else: print(f"Format: {result['format']}") # → 'EAN-13' print(f"Prefix: {result['prefix']}") # → '590' print(f"Issued by: {result['prefixCountry']}") # → 'Poland' # Detect books if result["prefix"] in ("978", "979"): print("This EAN encodes an ISBN (book)")
In a product import pipeline:
# Validate barcodes before inserting into a product catalog import re def import_products(rows: list[dict]) -> list[dict]: results = [] for row in rows: if not row.get("ean"): results.append({**row, "ean_status": "missing"}) continue check = validate_ean(row["ean"]) if not check["valid"]: results.append({**row, "ean_status": "invalid"}) continue results.append({ **row, "ean": re.sub(r"[\s-]", "", row["ean"]), # store normalised "ean_format": check["format"], "ean_prefix": check.get("prefix"), "ean_country": check.get("prefixCountry"), "ean_status": "valid", }) return results
format field to distinguish EAN-8 from EAN-13 — they can look similar in some fonts and your UI should render them differently.8. cURL example
Validate an EAN-13:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/ean?value=5901234123457"
Validate an EAN-8:
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/ean?value=96385074"
Book barcode (ISBN-encoded EAN-13):
curl -H "Authorization: Bearer YOUR_API_KEY" \ "https://api.isvalid.dev/v0/ean?value=9780141036144"
9. Understanding the response
Valid EAN-13:
{ "valid": true, "format": "EAN-13", "prefix": "590", "prefixCountry": "Poland" }
Valid EAN-8 (no prefix lookup for 8-digit codes):
{ "valid": true, "format": "EAN-8" }
Invalid barcode:
{ "valid": false }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Correct length (8 or 13), all digits, GS1 checksum passes |
| format | string | EAN-8 or EAN-13 |
| prefix | string | 2 or 3-digit GS1 prefix (EAN-13 only) |
| prefixCountry | string | GS1 member organisation associated with this prefix (EAN-13 only) |
10. Edge cases
UPC-A → EAN-13 conversion
If a user scans a US product with a UPC-A scanner, they may get 12 digits instead of 13. Convert UPC-A to EAN-13 by prepending a zero and re-validating.
def upc_a_to_ean13(upc: str) -> str: if len(upc) == 12: return "0" + upc return upc # already 13 digits or EAN-8 ean = upc_a_to_ean13("012345678905") print(ean) # → '0012345678905'
Distinguishing EAN-13 from ISBN-13
ISBN-13 and EAN-13 are the same format — ISBN-13 is just an EAN-13 with a 978 or 979 prefix. If your application handles both products and books, check the prefix to route the barcode to the correct lookup system (product catalogue vs book database).
Barcode scanner output
Barcode scanners typically emit the digits followed by Enter or Tab. Strip the trailing newline/tab before sending to the API. Some scanners add a prefix/suffix character — configure them to emit raw digits only for cleanest integration.
Leading zeros
EAN codes can start with zeros — 0012345678905 is a valid EAN-13. Do not parse barcodes as integers — store and process them as strings to preserve leading zeros.
Summary
Python integration notes
Pydantic V2's Annotated type with AfterValidator is the cleanest way to embed EAN barcode validation into your data models. Define the annotated type once and reuse it in FastAPI schemas, SQLModel ORM fields, or standalone parse calls. The validator function calls the IsValid API and raises a ValueError with the API's error message if the identifier is invalid, so Pydantic automatically maps it to a structured validation error in FastAPI's 422 response.
FastAPI and Django integration
In FastAPI, manage the httpx.AsyncClient lifecycle in the app's lifespan handler and inject it via Depends() to share the connection pool across all requests. In Django, add the EAN barcode check to a form field's validate() or a model's clean() method. For Django REST Framework, override to_internal_value() in a custom serializer field to call the SDK and raise serializers.ValidationError on failure.
Data pipelines that process EAN barcode values in bulk — ETL jobs, catalog imports, compliance checks — benefit from async concurrency. Use asyncio.gather() with a shared httpx.AsyncClient and an asyncio.Semaphore to cap concurrent connections. For Pandas or Polars workflows, apply validation column-by-column using df['column'].apply() with a synchronous wrapper, or run an async batch job that produces a validation result DataFrame that you merge back into your main DataFrame.
Normalise EAN barcode strings before calling the API: .strip() for whitespace, regex substitution to remove optional formatting characters, and .upper() where the format requires uppercase. Apply unicodedata.normalize('NFC', value) if the data may contain Unicode from multiple sources to prevent invisible character differences from causing false validation failures.
- Load
ISVALID_API_KEYviapython-dotenvand fail fast at startup if it is missing - Use
pytest-asynciowithasyncio_mode = "auto"for async validation tests; userespxto mockhttpxcalls - Cache validation results with
functools.lru_cachefor short-lived scripts or Redis for long-running services - Persist the full API response in a JSONB column alongside the raw identifier — enrichment fields save subsequent lookup calls
See also
Validate EAN barcodes instantly
Free tier includes 100 API calls per day. No credit card required. Supports EAN-8 and EAN-13 with GS1 prefix lookup.