IBAN Validation in Python: Complete Implementation with Regex and Check Digits
Two production-ready approaches: call the ibanchecker.cash API for bank metadata, or implement the MOD-97 algorithm yourself. Includes working code and pytest examples.
Technical Support, ibanchecker.cash
IBAN validation in Python requires more than a regular expression. A structurally valid IBAN must pass a MOD-97 check digit calculation, which no regex can perform. This guide covers two production-ready approaches: calling the ibanchecker.cash API for instant results with bank metadata, and implementing the full validation algorithm yourself when you need offline or embedded validation.
Why IBAN Validation Is More Than a Regex Check
An IBAN has three validation layers you must apply in order:
- Format check: The string must start with a valid two-letter country code and consist of alphanumeric characters only.
- Length check: Each country has a fixed IBAN length. A German (DE) IBAN is always 22 characters. A UK (GB) IBAN is always 22. A French (FR) IBAN is always 27. Length varies by country.
- MOD-97 check: The ISO 13616 check digit algorithm must produce a remainder of 1 when applied to the rearranged IBAN.
Skipping layer 3 means your validator accepts IBANs with typos, the entire point of the check digit is to catch single-character substitution and transposition errors.
Approach 1: Validate via the ibanchecker.cash API
The fastest path to production-grade IBAN validation is the ibanchecker.cash API. One POST call returns whether the IBAN is valid, the bank name, BIC, country, and branch data, all in under 100 ms from European infrastructure.
import requests
def validate_iban_api(iban: str) -> dict:
url = "https://ibanchecker.cash/api/v1/validate"
response = requests.post(
url,
json={"iban": iban},
headers={"Content-Type": "application/json"},
timeout=5,
)
response.raise_for_status()
return response.json()
# Example usage
result = validate_iban_api("DE89370400440532013000")
print(result)
# {
# "valid": true,
# "iban": "DE89 3704 0044 0532 0130 00",
# "country": "Germany",
# "bankName": "Deutsche Bank",
# "bic": "DEUTDEDB",
# "formatted": "DE89 3704 0044 0532 0130 00"
# }
if result["valid"]:
print(f"Bank: {result.get('bankName')}")
print(f"BIC: {result.get('bic')}")
else:
print(f"Invalid: {result.get('error')}")For bulk validation (up to 100 IBANs per request), use the bulk endpoint:
def validate_ibans_bulk(ibans: list[str]) -> list[dict]:
url = "https://ibanchecker.cash/api/v1/validate/bulk"
response = requests.post(
url,
json={"ibans": ibans},
headers={"Content-Type": "application/json"},
timeout=10,
)
response.raise_for_status()
return response.json()["results"]
results = validate_ibans_bulk([
"DE89370400440532013000",
"GB29NWBK60161331926819",
"FR7630006000011234567890189",
])
for r in results:
print(f"{r['iban']}: {'✓' if r['valid'] else '✗'}")Approach 2: Manual MOD-97 Implementation
When you need offline validation, embedded systems, batch processing pipelines without external dependencies, or compliance environments with no outbound network calls, implement the ISO 13616 algorithm directly. The implementation is about 30 lines of Python.
Step-by-Step Algorithm
- Normalize: strip whitespace, convert to uppercase.
- Move the first four characters to the end of the string.
- Replace each letter with its numeric equivalent: A=10, B=11 … Z=35.
- Compute the integer value of the resulting digit string modulo 97.
- The IBAN is valid if the remainder equals 1.
Country Length Table
You need a per-country length table to perform the length check. Here is a minimal version covering the most common IBAN countries:
IBAN_LENGTHS: dict[str, int] = {
"AD": 24, "AE": 23, "AL": 28, "AT": 20, "AZ": 28,
"BA": 20, "BE": 16, "BG": 22, "BH": 22, "BR": 29,
"BY": 28, "CH": 21, "CR": 22, "CY": 28, "CZ": 24,
"DE": 22, "DK": 18, "DO": 28, "EE": 20, "EG": 29,
"ES": 24, "FI": 18, "FO": 18, "FR": 27, "GB": 22,
"GE": 22, "GI": 23, "GL": 18, "GR": 27, "GT": 28,
"HR": 21, "HU": 28, "IE": 22, "IL": 23, "IQ": 23,
"IS": 26, "IT": 27, "JO": 30, "KW": 30, "KZ": 20,
"LB": 28, "LC": 32, "LI": 21, "LT": 20, "LU": 20,
"LV": 21, "LY": 25, "MC": 27, "MD": 24, "ME": 22,
"MK": 19, "MR": 27, "MT": 31, "MU": 30, "NL": 18,
"NO": 15, "PK": 24, "PL": 28, "PS": 29, "PT": 25,
"QA": 29, "RO": 24, "RS": 22, "SA": 24, "SC": 31,
"SE": 24, "SI": 19, "SK": 24, "SM": 27, "ST": 25,
"SV": 28, "TL": 23, "TN": 24, "TR": 26, "UA": 29,
"VA": 22, "VG": 24, "XK": 20,
}Full Validator Implementation
import re
IBAN_LENGTHS: dict[str, int] = {
"AD": 24, "AE": 23, "AL": 28, "AT": 20, "AZ": 28,
"BA": 20, "BE": 16, "BG": 22, "BH": 22, "BR": 29,
"BY": 28, "CH": 21, "CR": 22, "CY": 28, "CZ": 24,
"DE": 22, "DK": 18, "DO": 28, "EE": 20, "EG": 29,
"ES": 24, "FI": 18, "FO": 18, "FR": 27, "GB": 22,
"GE": 22, "GI": 23, "GL": 18, "GR": 27, "GT": 28,
"HR": 21, "HU": 28, "IE": 22, "IL": 23, "IQ": 23,
"IS": 26, "IT": 27, "JO": 30, "KW": 30, "KZ": 20,
"LB": 28, "LC": 32, "LI": 21, "LT": 20, "LU": 20,
"LV": 21, "LY": 25, "MC": 27, "MD": 24, "ME": 22,
"MK": 19, "MR": 27, "MT": 31, "MU": 30, "NL": 18,
"NO": 15, "PK": 24, "PL": 28, "PS": 29, "PT": 25,
"QA": 29, "RO": 24, "RS": 22, "SA": 24, "SC": 31,
"SE": 24, "SI": 19, "SK": 24, "SM": 27, "ST": 25,
"SV": 28, "TL": 23, "TN": 24, "TR": 26, "UA": 29,
"VA": 22, "VG": 24, "XK": 20,
}
def normalize_iban(raw: str) -> str:
return raw.replace(" ", "").replace("-", "").upper()
def validate_iban(raw: str) -> tuple[bool, str]:
"""
Returns (is_valid, error_message).
error_message is empty string when valid.
"""
iban = normalize_iban(raw)
if not re.match(r"^[A-Z]{2}[0-9]{2}[A-Z0-9]+$", iban):
return False, "Invalid format: must start with country code and two digits"
country = iban[:2]
expected_length = IBAN_LENGTHS.get(country)
if expected_length is None:
return False, f"Unknown country code: {country}"
if len(iban) != expected_length:
return False, f"Wrong length: expected {expected_length}, got {len(iban)}"
rearranged = iban[4:] + iban[:4]
numeric = "".join(
str(ord(c) - ord("A") + 10) if c.isalpha() else c
for c in rearranged
)
if int(numeric) % 97 != 1:
return False, "Invalid check digits (MOD-97 failed)"
return True, ""
def format_iban(iban: str) -> str:
normalized = normalize_iban(iban)
return " ".join(normalized[i:i+4] for i in range(0, len(normalized), 4))Edge Cases You Must Handle
Real-world IBAN input arrives in many forms. Your normalizer must handle all of them before any structural check runs:
- Spaces:
DE89 3704 0044 0532 0130 00, printed format from bank statements. Strip all spaces. - Lowercase:
de89370400440532013000, common in API payloads. Convert to uppercase before checking. - Dashes:
DE89-3704-0044-0532-0130-00, rare but appears in some ERP exports. Strip dashes. - Leading/trailing whitespace: Strip before processing.
- Non-ASCII characters: Reject, an IBAN contains only A–Z and 0–9.
def normalize_iban(raw: str) -> str:
return raw.strip().replace(" ", "").replace("-", "").upper()
# All four of these produce the same normalized form:
assert normalize_iban("DE89 3704 0044 0532 0130 00") == "DE89370400440532013000"
assert normalize_iban("de89370400440532013000") == "DE89370400440532013000"
assert normalize_iban("DE89-3704-0044-0532-0130-00") == "DE89370400440532013000"
assert normalize_iban(" DE89370400440532013000 ") == "DE89370400440532013000"Testing with pytest
A complete test suite covers valid IBANs, structural failures, and edge cases:
import pytest
from iban_validator import validate_iban, normalize_iban
VALID_IBANS = [
"DE89370400440532013000",
"GB29NWBK60161331926819",
"FR7630006000011234567890189",
"NL91ABNA0417164300",
"BE68539007547034",
]
INVALID_IBANS = [
("DE89370400440532013001", "check digits"), # last digit wrong
("DE8937040044053201300", "wrong length"), # 21 chars instead of 22
("XX89370400440532013000", "unknown country"), # XX not in table
("", "empty string"),
("not-an-iban", "invalid format"),
]
@pytest.mark.parametrize("iban", VALID_IBANS)
def test_valid_ibans(iban):
valid, error = validate_iban(iban)
assert valid, f"Expected valid but got: {error}"
@pytest.mark.parametrize("raw,label", INVALID_IBANS)
def test_invalid_ibans(raw, label):
valid, _ = validate_iban(raw)
assert not valid, f"Expected invalid for {label}"
def test_normalizes_spaces():
valid, _ = validate_iban("DE89 3704 0044 0532 0130 00")
assert valid
def test_normalizes_lowercase():
valid, _ = validate_iban("de89370400440532013000")
assert valid
def test_normalizes_dashes():
valid, _ = validate_iban("DE89-3704-0044-0532-0130-00")
assert validRun with pytest -v iban_test.py. All tests should pass with the implementation above.
Which Approach Should You Use?
Use the ibanchecker.cash API when:
- You need bank name, BIC, or branch data alongside the validity result.
- You want automatic updates when new countries join the IBAN standard.
- You process IBANs in a web service or SaaS application with outbound network access.
Use the manual implementation when:
- You process IBANs in an air-gapped or offline environment.
- You have very high volume (millions per day) and latency budgets are tight.
- You need to embed validation in a mobile app, CLI tool, or data pipeline with no runtime dependencies.
For most production applications, the API is the correct default. The manual implementation is a solid fallback or complement, add it as a local pre-filter before calling the API to avoid paying API costs on obviously malformed input.
Sources & References
- SWIFT IBAN Registry — Official IBAN country format specifications
- ISO 13616-1:2020 — International standard for IBAN structure
- schwifty (PyPI) — Python library for IBAN and BIC validation
- python-stdnum (PyPI) — Python library for validating IBAN and other standard numbers
Last updated: June 2026
Integrate IBAN validation via API
REST API with 1,000 free validations/month. JSON response with bank name, SEPA status, and BBAN breakdown.
How to Validate IBANs in JavaScript (API & Manual)
Two approaches: call the ibanchecker.cash API for full bank data, or implement MOD-97 validation yourself. Includes work...
Building an IBAN Validation API: REST Endpoint Design and Edge Cases
Architect-level guide to IBAN API design — GET vs POST tradeoffs, error code standards, normalization contracts, KV cach...
IBAN Regex Patterns That Actually Work (And Why Simple Regex Isn't Enough)
Country-specific regex patterns for DE, GB, FR, NL plus a generic IBAN regex — and why regex is only step 1 of validatio...
Top IBAN Validation Libraries for Node.js, Python, PHP, and Go in 2026
Comparison of ibantools, python-stdnum, php-iban, and go-iban — features, MOD-97 support, BIC lookup, TypeScript support...