Credit Card Validator — Python source
Validate credit card numbers with the Luhn checksum and detect the issuing brand (Visa, Mastercard, Amex, Discover, Diners, JCB, UnionPay, Maestro). 100% client-side - nothing is transmitted.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""credit-card-validator — Python.
CosmoDev polyglot showcase port of the credit-card-validator tool,
ported from src/lib/creditCard.ts.
Display source — part of CosmoDev's polyglot tool pages.
Pure credit-card validation logic with no I/O: sanitize to digits, run the
Luhn checksum, detect the issuing brand by prefix + length, and group the
number for display. Never raises on malformed input.
NOTE on `\d`: Python's `re` module treats `\d` as Unicode decimal digits by
default, whereas JavaScript / Go / Rust treat it as ASCII [0-9]. To stay
byte-for-byte equivalent with the TypeScript source, every pattern here uses
explicit [0-9] ranges instead of \d / \D.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
@dataclass
class CardInfo:
"""Full validation result. ``brand`` is None when the issuer is unknown."""
input: str
sanitized: str # digits only
valid: bool # digits present, sane length, and Luhn passes
luhn_valid: bool # Luhn checksum passes (requires >= 12 digits)
brand: str | None # issuing brand, or None when unknown
formatted: str # grouped per brand spacing rules
def luhn_check(digits: str) -> bool:
"""Luhn (mod-10) checksum. Empty or non-digit input returns False."""
if not digits or re.fullmatch(r"[0-9]*", digits) is None:
return False
total = 0
should_double = False
# Walk right-to-left; double every second digit and collapse >9 to its digit-sum
# (e.g. 7*2=14 -> 14-9=5, identical to summing 1+4).
for ch in reversed(digits):
d = ord(ch) - 48 # ord("0") == 48
if should_double:
d *= 2
if d > 9:
d -= 9
total += d
should_double = not should_double
return total % 10 == 0
# Brand detection table. ORDER MATTERS: the first prefix+length match wins.
# Maestro is last because its broad "6" / 5x ranges overlap narrower issuers.
_BRAND_RULES: list[tuple[str, re.Pattern[str], tuple[int, ...]]] = [
("Visa", re.compile(r"^4"), (13, 16, 19)),
("Mastercard", re.compile(r"^(5[1-5]|2(2[2-9]|[3-6][0-9]|7[01]|720))"), (16,)),
("American Express", re.compile(r"^3[47]"), (15,)),
(
"Discover",
re.compile(r"^(6011|65|64[4-9]|622(12[6-9]|1[3-9][0-9]|[2-8][0-9][0-9]|9([01][0-9]|2[0-5])))"),
(16, 19),
),
("Diners Club", re.compile(r"^(30[0-5]|3095|36|38|39|54|55)"), (14, 16)),
("JCB", re.compile(r"^35(2[89]|[3-8][0-9])"), (16, 17, 18, 19)),
("UnionPay", re.compile(r"^62"), (16, 17, 18, 19)),
("Maestro", re.compile(r"^(50|56|57|58|6)"), tuple(range(12, 20))),
]
def detect_brand(digits: str) -> str | None:
"""Detect the card brand by prefix + length. Returns None when unknown."""
if not digits:
return None
for name, pattern, lengths in _BRAND_RULES:
if pattern.search(digits) and len(digits) in lengths:
return name
return None
def _group(digits: str, widths: tuple[int, ...]) -> str:
"""Slice digits into runs of the given widths, joining with spaces."""
parts: list[str] = []
i = 0
n = len(digits)
for w in widths:
if i >= n:
break
parts.append(digits[i : i + w])
i += w
# Any trailing remainder (e.g. a partially typed number) is appended as-is.
if i < n:
parts.append(digits[i:])
return " ".join(parts)
def format_card(digits: str, brand: str | None) -> str:
"""Group the number per brand spacing rules."""
if not digits:
return ""
# Amex uses a 4-6-5 grouping; Diners Club's 14-digit variant uses 4-6-4.
if brand == "American Express":
return _group(digits, (4, 6, 5))
if brand == "Diners Club" and len(digits) == 14:
return _group(digits, (4, 6, 4))
# Default: groups of four separated by spaces; the lookahead (?=.) drops the trailing space.
return re.sub(r"(.{4})(?=.)", r"\1 ", digits).strip()
def validate_card(input: str) -> CardInfo:
"""Validate a card number. Always returns a CardInfo; never raises."""
sanitized = re.sub(r"[^0-9]+", "", input)
brand = detect_brand(sanitized)
# Luhn is only meaningful once we have a plausible card length (>= 12 digits).
luhn_valid = len(sanitized) >= 12 and luhn_check(sanitized)
return CardInfo(
input=input,
sanitized=sanitized,
valid=luhn_valid,
luhn_valid=luhn_valid,
brand=brand,
formatted=format_card(sanitized, brand),
)
if __name__ == "__main__":
# Tiny showcase: validate a well-known test number (Visa).
info = validate_card("4111 1111 1111 1111")
print(f"brand={info.brand} valid={info.valid} formatted={info.formatted!r}")
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →