Skip to content

IBAN Validator — Python source

Validate International Bank Account Numbers (IBAN) with the mod-97 checksum, verify the country-specific length, and format the result. 100% client-side, no network.

This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.

"""Pure IBAN validation logic — ISO 13616 mod-97 checksum.

Language: Python
CosmoDev polyglot showcase port of the ``iban-validator`` tool.
Ported from src/lib/iban.ts — display source, part of CosmoDev's
polyglot tool pages.

Normalizes the input (uppercase, strip whitespace/dashes), checks the
structural regex (country code + 2 check digits + 1-30 BBAN chars),
verifies the per-country length, and runs the ISO 13616 mod-97 checksum:
move the first 4 chars to the end, map A=10..Z=35, and confirm the
resulting integer is congruent to 1 mod 97. Python ints are arbitrary
precision, but the remainder is folded one digit at a time (it stays
under 97) to mirror the reference algorithm exactly. validate_iban never
raises — it reports failures through the ``error`` field.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Optional

# Per-country IBAN lengths (ISO 13616) — a representative subset.
IBAN_LENGTHS: dict[str, int] = {
    "AL": 28, "AD": 24, "AT": 20, "AZ": 28, "BH": 22, "BY": 28, "BE": 16,
    "BA": 20, "BR": 29, "BG": 22, "CR": 22, "HR": 21, "CY": 28, "CZ": 24,
    "DK": 18, "DO": 28, "EE": 20, "FO": 18, "FI": 18, "FR": 27, "GE": 22,
    "DE": 22, "GI": 23, "GR": 27, "GL": 18, "GT": 28, "HU": 28, "IS": 26,
    "IE": 22, "IL": 23, "IT": 27, "JO": 30, "KZ": 20, "XK": 20, "KW": 30,
    "LV": 21, "LB": 28, "LI": 21, "LT": 20, "LU": 20, "MK": 19, "MT": 31,
    "MR": 27, "MU": 30, "MC": 27, "MD": 24, "ME": 22, "NL": 18, "NO": 15,
    "PK": 24, "PS": 29, "PL": 28, "PT": 25, "QA": 29, "RO": 24, "LC": 32,
    "SM": 27, "ST": 25, "SA": 24, "RS": 22, "SC": 31, "SK": 24, "SI": 19,
    "SG": 19, "ES": 24, "SE": 24, "CH": 21, "TL": 23, "TN": 24, "TR": 26,
    "UA": 29, "AE": 23, "GB": 22, "VG": 24,
}

_STRUCTURE = re.compile(r"^[A-Z]{2}[0-9]{2}[A-Z0-9]{1,30}$")
_CC_HEAD = re.compile(r"^[A-Z]{2}")


@dataclass
class IbanInfo:
    """Mirrors the TypeScript lib's result shape."""
    input: str
    cleaned: str
    country_code: Optional[str] = None
    valid: bool = False
    checksum_ok: bool = False
    length_ok: bool = False
    expected_length: Optional[int] = None
    formatted: str = ""
    error: Optional[str] = None


def mod97_check(cleaned: str) -> bool:
    """ISO 13616 mod-97 checksum over a CLEANED iban (uppercase, no spaces)."""
    # Move the first 4 chars (country + check) to the end.
    rearranged = cleaned[4:] + cleaned[:4]
    numeric: list[str] = []
    for ch in rearranged:
        code = ord(ch)
        if 48 <= code <= 57:
            numeric.append(ch)
        elif 65 <= code <= 90:
            numeric.append(str(code - 55))  # A=10 .. Z=35
        else:
            return False  # invalid character
    rem = 0
    for ch in "".join(numeric):
        rem = (rem * 10 + (ord(ch) - 48)) % 97
    return rem == 1


def validate_iban(input: str) -> IbanInfo:
    """Validate an IBAN. Always returns IbanInfo; never raises."""
    cleaned = re.sub(r"[\s-]", "", (input or "").upper())
    cc = cleaned[:2] if _CC_HEAD.match(cleaned) else None
    expected = IBAN_LENGTHS.get(cc) if cc else None

    info = IbanInfo(
        input=input or "",
        cleaned=cleaned,
        country_code=cc,
        expected_length=expected,
        formatted=re.sub(r"(.{4})(?=.)", r"\1 ", cleaned).strip(),
    )

    if not _STRUCTURE.match(cleaned):
        info.error = "Invalid IBAN format."
        return info

    length_ok = expected is None or len(cleaned) == expected
    checksum_ok = mod97_check(cleaned)
    info.length_ok = length_ok
    info.checksum_ok = checksum_ok

    if not length_ok:
        info.error = f"Length should be {expected} for {cc}."
        return info
    if not checksum_ok:
        info.error = "Checksum failed."
        return info
    info.valid = True
    return info


if __name__ == "__main__":
    # GB82 WEST 1234 5698 7654 32 — a known-good reference IBAN.
    print(validate_iban("GB82WEST12345698765432"))

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 →