Python ile IBAN Doğrulama: Saf Fonksiyon ve Testler
Elli satırlık saf bir Python modülü IBAN'ın biçimini ve kontrol hanelerini güvenilir şekilde sınar. Asıl iş, hata nedenlerini ve testleri doğru kurmakta.
IBAN Aracı Editör Ekibi5 dk okuma

Python'da IBAN doğrulamak için bir kütüphane kurmanız gerekmez; uzunluk tablosu ve MOD 97 hesabı toplam elli satır civarında saf bir modüle sığar. Zor olan kodu yazmak değil, neyi doğruladığınızı doğru adlandırmak ve hatalı girdiye makinece okunabilir bir neden döndürmektir. Aşağıdaki modül ve testleri Python 3.9 ile çalıştırdık; test dosyasındaki tüm durumlar geçiyor.
Başlamadan bir sınır: bu fonksiyon IBAN'ın biçimini ve kontrol hanelerini sınar. Hesabın açık olduğunu, kime ait olduğunu ya da o hesaba para gidip gitmeyeceğini bilemez. Fonksiyonun adında, dönüş değerinde ve kullanıcıya gösterdiğiniz mesajda bu ayrımı koruyun. Neden önemli olduğunu geçerli IBAN, aktif hesap mı? rehberinde anlattık.
Neden yalnızca regex yetmez?
İnternette sık rastlanan kısa çözüm, TR ile başlayıp 24 rakamla devam eden bir düzenli ifadedir. Şekli kontrol eder, o kadar:
import re
TR_IBAN = re.compile(r"TR\d{24}")
print(bool(TR_IBAN.fullmatch("TR140001005001234567890123"))) # True
print(bool(TR_IBAN.fullmatch("TR150001005001234567890123"))) # True, ama kontrol hanesi yanlışİkinci satırdaki IBAN'ın kontrol haneleri yanlış, ama regex bunu göremez. Bir hanesi yanlış yazılmış, iki rakamı yer değiştirmiş ya da kopyalarken tek karakteri bozulmuş bir IBAN, uzunluğu doğru olduğu sürece bu kalıptan geçer. Kontrol hanelerinin varlık sebebi tam olarak bu hataları yakalamaktır. Regex'i ilk eleme için kullanın; tek kontrol olarak kullanmayın, çünkü size yanlış bir güven verir ve hata ancak ödeme reddedildiğinde ortaya çıkar.
Saf fonksiyon: normalize, şekil, uzunluk, MOD 97
import re
# ISO 13616 kayıt defterinden (SWIFT IBAN Registry) seçilmiş birkaç ülke.
# Gerçek projede tabloyu tam listeyle ve düzenli güncellenen bir kaynaktan doldurun.
IBAN_LENGTHS = {
"TR": 26, "DE": 22, "GB": 22, "FR": 27, "NL": 18,
"AT": 20, "BE": 16, "IT": 27, "ES": 24, "CH": 21,
}
_CLEAN = re.compile(r"[\s\-.]")
_SHAPE = re.compile(r"[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}")
class IbanError(ValueError):
"""IBAN biçimsel olarak geçersiz; .code makinece okunabilir neden."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def normalize(raw: str) -> str:
"""Boşluk, tire ve noktayı atar, büyük harfe çevirir."""
return _CLEAN.sub("", raw).upper()
def mod97(iban: str) -> int:
"""ISO 7064 MOD 97-10 kalanı; büyük sayı üretmeden hane hane hesaplar."""
rearranged = iban[4:] + iban[:4]
remainder = 0
for ch in rearranged:
value = int(ch, 36) # '0'-'9' -> 0-9, 'A'-'Z' -> 10-35
remainder = (remainder * (10 if value < 10 else 100) + value) % 97
return remainder
def validate_iban(raw: str) -> str:
"""Geçerliyse elektronik formatı döndürür, değilse IbanError fırlatır.
Yalnızca biçim kontrolüdür: hesabın var olduğunu veya kime ait
olduğunu doğrulamaz.
"""
iban = normalize(raw)
if not iban.isascii() or not _SHAPE.fullmatch(iban):
raise IbanError("FORMAT", "IBAN harf-harf-rakam-rakam ile başlamalı, 15-34 karakter olmalı.")
expected = IBAN_LENGTHS.get(iban[:2])
if expected is None:
raise IbanError("COUNTRY", f"{iban[:2]} için uzunluk bilgisi yok.")
if len(iban) != expected:
raise IbanError("LENGTH", f"{iban[:2]} IBAN'ı {expected} karakter olmalı, {len(iban)} geldi.")
if mod97(iban) != 1:
raise IbanError("CHECKSUM", "Kontrol haneleri tutmuyor; bir karakter hatalı olabilir.")
return ibanKodda bilinçli birkaç tercih var. validate_iban geçerli girdide True yerine normalize edilmiş IBAN'ı döndürüyor. Çağıran taraf hemen ardından veritabanına yazacaksa, kullanıcının girdiği boşluklu hâli değil elektronik formatı yazmalı; dönüş değeri bunu doğal hâle getiriyor. Geçersiz girdide ise IbanError fırlatıyor ve code alanında FORMAT, COUNTRY, LENGTH veya CHECKSUM taşıyor. API cevabında ya da form hatasında insan için yazılmış mesajı değil bu kodu esas alın; mesaj metni değişebilir, kod değişmemeli.
mod97 fonksiyonu 28 haneli dev sayıyı hiç kurmuyor. Python'un tamsayıları sınırsız olduğu için int(...) % 97 da doğru sonuç verirdi; buna rağmen hane hane yöntemi tercih ettik. Aynı mantık JavaScript'e, SQL'e ya da bir tablo formülüne taşındığında da çalışır ve sitedeki JavaScript IBAN doğrulama kodu ile aynı kalanı üretir. int(ch, 36) çağrısı rakamları 0-9, harfleri 10-35 değerine çevirir. İki haneli bir değer eklenirken kalanı 100 ile çarpmak, harfin iki rakamını dizeye yazıp birleştirmekle aynı şeydir.
isascii() kontrolü göründüğünden önemli. Python'da str.upper() Türkçe İ harfini olduğu gibi bırakır ve bazı Unicode rakamları int() tarafından kabul edilebilir. Düzenli ifade ASCII dışı karakteri zaten reddeder, ama bu tür girdiyi en başta açıkça kesmek hem okuyana niyeti anlatır hem de ileride regex'i değiştiren birinin açtığı deliği kapatır.
Normalize ederken ne kadar hoşgörülü olmalı?
normalize yalnızca boşluk, tire ve noktayı atıyor. Harf ve rakam dışındaki her şeyi silmek daha "kullanıcı dostu" görünebilir, ama bunu yapmıyoruz. Bir IBAN'ın içinde yıldız, eğik çizgi ya da virgül varsa, girdi büyük olasılıkla bozuk bir kaynaktan geliyordur: maskelenmiş bir ekran görüntüsünden kopyalanmış, iki alan birleştirilmiş ya da bir CSV sütunu kaymış olabilir. Bu karakterleri sessizce silip kalan dizi tesadüfen MOD 97'den geçerse, hatayı saklamış olursunuz. FORMAT hatası vermek kullanıcıyı kaynağa geri gönderir; bu da istediğimiz davranış.
Görünmez karakterler ayrı bir konu. Web sayfalarından kopyalanan metinde bölünemez boşluk sık görülür ve Python 3'te \s bu karakteri de kapsadığı için temizlenir. Sıfır genişlikli boşluk gibi karakterler ise \s kapsamına girmez ve şekil kontrolüne takılır. Bunları da kabul etmek istiyorsanız temizlik ifadesine açıkça ekleyin ve bunun için ayrı bir test yazın. Bir kural daha: hata mesajında kullanıcının girdiği IBAN'ı olduğu gibi geri yansıtmayın. API cevapları ve hata logları çoğu zaman beklediğinizden daha fazla yere kopyalanır; hangi karakterin sorunlu olduğunu söylemek için tam numarayı tekrar etmeniz gerekmez.
Ülke uzunluk tablosu nereden gelmeli?
Örnekteki sözlük yalnızca on ülke içeriyor ve bu bilerek yapıldı. IBAN kullanan ülkelerin yapısı ve uzunluğu, ISO 13616 standardı adına Swift'in yayımladığı IBAN Registry belgesinde tutuluyor. Bu kayıt zaman zaman güncelleniyor; yeni ülke eklenebiliyor ya da bir ülkenin yapısı değişebiliyor. Tabloyu blog yazılarından veya hafızadan doldurmayın. Registry'den bir kez üretin, kaynağını ve tarihini yorum satırına yazın, belirli aralıklarla karşılaştırın. Hızlı bakış için ülke IBAN formatları sayfası da işinizi görür.
Tabloda olmayan ülkeyle ne yapacağınız ürün kararıdır. Bizim örneğimiz COUNTRY hatası verip reddediyor. Yalnızca Türkiye ve birkaç Avrupa ülkesiyle çalışan bir bordro sistemi için doğru tercih budur. Her ülkeden ödeme alan bir platformda ise bilinmeyen ülke için yalnızca MOD 97 yapıp uyarı döndürmek daha makul olabilir. Hangisini seçerseniz seçin, bunu test ile sabitleyin.
Testler: hangi durumları kilitlemeli?
import pytest
from iban_check import IbanError, mod97, normalize, validate_iban
@pytest.mark.parametrize("raw", [
"TR140001005001234567890123",
"TR14 0001 0050 0123 4567 8901 23",
"de89 3704 0044 0532 0130 00",
"GB82WEST12345698765432",
])
def test_gecerli(raw):
assert validate_iban(raw) == normalize(raw)
@pytest.mark.parametrize("raw, code", [
("TR150001005001234567890123", "CHECKSUM"),
("TR14000100500123456789012", "LENGTH"),
("TR14O001005001234567890123", "CHECKSUM"),
("14TR0001005001234567890123", "FORMAT"),
("TR14000100500123456789012İ", "FORMAT"),
("XX140001005001234567890123", "COUNTRY"),
])
def test_gecersiz(raw, code):
with pytest.raises(IbanError) as exc:
validate_iban(raw)
assert exc.value.code == code
def test_kontrol_hanesi_hesabi_ile_uyumlu():
bban = "0001005001234567890123"
check = 98 - mod97("TR00" + bban)
assert f"TR{check:02d}{bban}" == "TR140001005001234567890123"Test listesi rastgele seçilmedi. Boşluklu ve küçük harfli yazımlar, kullanıcıların gerçekte yapıştırdığı hâllerdir. TR14O001... satırında sıfır yerine büyük O harfi var; şekil kontrolünden geçiyor ama MOD 97'ye takılıyor. Elle yazımda bu karışıklığın ne kadar sık görüldüğünü IBAN yazım kontrolü aracında görebilirsiniz. Son test, kontrol hanesi üretiminin doğrulamayla tutarlı olduğunu gösteriyor: hesap bölümünü alıp kontrol hanesini 98 - kalan formülüyle hesaplayınca örnek IBAN'ı birebir geri elde ediyoruz. Testleri pytest ile çalıştırın.
Bir uyarı: gerçek müşteri IBAN'larını test dosyasına koymayın. Test verisi depoya girer, depo kopyalanır ve oradan silinmesi zordur. Yazıdaki IBAN'lar kamuya açık örnek numaralardır.
Hazır kütüphaneler: ne zaman mantıklı?
PyPI'da IBAN işleyen birkaç olgun paket var. python-stdnum IBAN dahil çok sayıda standart numara biçimini doğrular ve LGPL lisanslıdır. schwifty IBAN ve BIC ayrıştırmaya odaklanır, bazı ülkeler için banka bilgisi de sunar ve MIT lisanslıdır. Django kullanıyorsanız django-localflavor paketinde IBANField model alanı ve IBANFormField form alanı bulunur. Örnek olarak python-stdnum ile aynı iki IBAN:
from stdnum import iban
print(iban.is_valid("TR14 0001 0050 0123 4567 8901 23")) # True
print(iban.is_valid("TR150001005001234567890123")) # FalseKütüphane seçmeden önce üç soruya bakın. Lisansı projenizle uyumlu mu? Ülke tablosunu ne sıklıkla güncelliyor ve siz o güncellemeyi alacak mısınız? Size gereken tek şey MOD 97 ve uzunluksa, bir bağımlılığın güvenlik ve bakım yükünü üstlenmeye değer mi? Banka adı çözümleme gibi ek özelliklere ihtiyacınız yoksa yukarıdaki elli satır çoğu zaman yeterlidir. İhtiyacınız varsa tekerleği yeniden icat etmeyin, ama kütüphanenin döndürdüğü banka bilgisinin bir sözlükten geldiğini, hesabı sorgulamadığını da bilin.
Toplu işte dikkat edilecekler
Bir CSV'deki binlerce satırı doğrularken hatalı satırda döngüyü durdurmayın; IbanError'ı yakalayıp satır numarası ve code ile bir rapora yazın. Rapora IBAN'ın tamamını değil maskeli hâlini koyun; log dosyaları çoğu zaman veritabanından daha geniş bir ekibin eline geçer. Kaydetme tarafındaki ayrıntılar için IBAN veritabanında nasıl saklanmalı? yazısına bakın. CSV Excel'de açılıp kaydedildiyse IBAN'lar bozulmuş olabilir; Python'a gelmeden önce Excel IBAN temizleme aracıyla sütunu kontrol etmek, hatanın kaynağını ayırmanızı kolaylaştırır.
Kaynaklar
- Swift: IBAN (ISO 13616) ve IBAN Registry
- Python belgeleri: int() ve taban dönüşümü
- Python belgeleri: str.isascii()
- PyPI: python-stdnum
- PyPI: schwifty
- PyPI: django-localflavor
Yazıların nasıl hazırlanıp güncellendiğini metodoloji sayfasında anlatıyoruz. Hata gördüyseniz bize yazın.
Sık sorulan sorular
Hayır. Fonksiyon yalnızca karakter yapısını, ülkeye göre uzunluğu ve MOD 97 kontrol hanelerini sınar. Hesabın açık olup olmadığını veya sahibini bilemez; bu bilgi yalnızca bankanın kendi sistemlerindedir.


