Neden önemli?
Temel seviyede try/except ile hatayı yakaladın. Ama Ilgaz Bank'ın transfer servisinde "bir hata oldu" demek yetmez: yetersiz bakiye mi, hesap bulunamadı mı, günlük limit mi aşıldı? Çağıran kod bunları ayırt edip farklı tepki vermeli: birinde müşteriye mesaj, diğerinde operasyon ekibine alarm. Hata tasarımı bu ayrımı kodun bir parçası yapar: anlamlı istisna sınıfları, nedeni koruyan zincirleme, sonradan araştırılabilen log kayıtları. Üretimde gece üçte uyandıran hataların çoğu, kötü yakalanmış ve sessizce yutulmuş istisnalardır.
Özel istisna hiyerarşisi
Exception'dan türeyen bir temel sınıf ve ondan türeyen özel sınıflar tanımlarsın. Hiyerarşi sayesinde çağıran, ister tek tek ister hepsini birden yakalar. Gövde genellikle boştur (pass ya da docstring); gerekirse ek nitelik taşır.
class BankaHatasi(Exception):
"""Ilgaz Bank iş kurallarının temel hatası."""
class HesapBulunamadi(BankaHatasi):
def __init__(self, no):
super().__init__(f"hesap bulunamadı: {no}")
self.no = no
class YetersizBakiye(BankaHatasi):
def __init__(self, hesap_no, istenen, mevcut):
super().__init__(f"{hesap_no}: {istenen} istendi, {mevcut} var")
self.eksik = istenen - mevcut
class LimitAsildi(BankaHatasi):
pass
hesaplar = {"TR01": 500.0, "TR02": 12000.0}
def cek(no, tutar):
if no not in hesaplar:
raise HesapBulunamadi(no)
if tutar > 10000:
raise LimitAsildi(f"günlük limit 10000, istenen {tutar}")
if tutar > hesaplar[no]:
raise YetersizBakiye(no, tutar, hesaplar[no])
hesaplar[no] -= tutar
return hesaplar[no]
for no, t in [("TR01", 100), ("TR09", 50), ("TR01", 900), ("TR02", 11000)]:
try:
print("Kalan:", cek(no, t))
except YetersizBakiye as e:
print("Müşteriye:", e, "| eksik:", e.eksik)
except BankaHatasi as e: # kalan tüm banka hataları
print("Operasyona:", type(e).__name__, "-", e)
YetersizBakiye) temel sınıftan (BankaHatasi) önce yaz; tersini yazarsan özel blok hiç çalışmaz.Hiyerarşi nasıl kurulur?
| Katman | Örnek | Kim yakalar |
|---|---|---|
| Kök | BankaHatasi(Exception) | En dış katman: "bizim hatamız mı, Python'ın mı" |
| Alan | HesapHatasi, TransferHatasi | Servis katmanı: alana göre tepki |
| Somut | YetersizBakiye, HesapBulunamadi | Kullanıcı arayüzü: anlamlı mesaj |
| Yerleşik | ValueError, KeyError | Genel programlama hataları; iş hatasına çevrilir |
raise from: nedeni koru
Alt seviyede KeyError ya da ValueError fırladı, sen bunu iş diline çevirmek istiyorsun. raise YeniHata(...) from eski yazarsan Python iki hatayı zincirler: iz kaydında "The above exception was the direct cause of the following exception" satırıyla asıl neden görünür. from None ise zinciri bilinçli olarak keser (nedeni gizlemek istediğin, örneğin kullanıcıya iç detay sızdırmamak gerektiği durumlarda).
class VeriHatasi(Exception):
pass
def tutar_oku(kayit):
try:
return float(kayit["tutar"])
except KeyError as e:
raise VeriHatasi(f"kayıtta 'tutar' alanı yok: {kayit}") from e
except ValueError as e:
raise VeriHatasi(f"tutar sayı değil: {kayit['tutar']!r}") from e
for k in [{"tutar": "12.5"}, {"fiyat": 3}, {"tutar": "abc"}]:
try:
print(tutar_oku(k))
except VeriHatasi as e:
print("VeriHatasi:", e)
print(" neden:", type(e.__cause__).__name__, "-", e.__cause__)
raise yaz (argümansız); aynı istisna izi kaybolmadan devam eder. raise e yazmak da çalışır ama iz kaydına gereksiz bir satır ekler.logging: print değil
Üretim kodunda print ile hata takibi yapılmaz: seviyesi yok, zamanı yok, kapatılamaz, dosyaya yönlendirilemez. logging modülü bunları verir. Beş seviye vardır: DEBUG < INFO < WARNING < ERROR < CRITICAL; yapılandırılan eşiğin altındakiler yazılmaz. Bu ortamda çıktıyı stdout'a yönlendiriyoruz.
import logging, sys
logging.basicConfig(
stream=sys.stdout,
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger("anadolu.transfer")
def transfer(gonderen, alici, tutar):
log.info("transfer başlıyor %s -> %s, %.2f", gonderen, alici, tutar)
if tutar > 10000:
log.warning("yüksek tutar, ek onay gerekebilir: %.2f", tutar)
log.debug("bu satır görünmez: seviye INFO")
return True
transfer("TR01", "TR02", 250)
transfer("TR01", "TR03", 25000)
Dikkat: mesajı %s ile ve argümanları ayrı veriyoruz, f-string ile değil. Böylece seviye kapalıysa dize hiç biçimlenmez ve log kayıtları toplayan araçlar (Sentry, ELK) aynı mesaj şablonunu gruplayabilir.
İstisnayı logla: exception()
except bloğunda log.exception("...") çağırırsan mesajla birlikte tam iz kaydı (traceback) yazılır; seviye ERROR'dur. Hatayı yutmadan loglayıp yeniden yükseltmek en sağlıklı kalıptır.
import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("anadolu.rapor")
def kayit_isle(kayit):
try:
return kayit["tutar"] / kayit["adet"]
except ZeroDivisionError:
log.exception("adet sıfır, kayıt atlandı: %s", kayit)
return None
print(kayit_isle({"tutar": 100, "adet": 4}))
print(kayit_isle({"tutar": 100, "adet": 0}))
log.exception yaz; mümkünse yakaladığın hatayı dar tut (except KeyError) ve gerisini yukarı bırak.EAFP ile LBYL
İki yaklaşım: LBYL (Look Before You Leap, önce bak sonra atla) koşulla kontrol eder; EAFP (Easier to Ask Forgiveness than Permission, izin istemektense af dile) dener ve hatayı yakalar. Python topluluğu çoğunlukla EAFP'yi tercih eder: daha az kod, daha az yarış durumu (kontrol ile kullanım arasında dünya değişebilir), ve istisna yolu nadir durumsa daha hızlıdır.
musteriler = {"M1": {"ad": "Ayşe", "eposta": "ayse@mail.com"}, "M2": {"ad": "Can"}}
# LBYL
def eposta_lbyl(no):
if no in musteriler and "eposta" in musteriler[no]:
return musteriler[no]["eposta"]
return None
# EAFP
def eposta_eafp(no):
try:
return musteriler[no]["eposta"]
except KeyError:
return None
for no in ["M1", "M2", "M9"]:
print(no, eposta_lbyl(no), eposta_eafp(no))
# Tip kontrolü yerine davranışa güven (ördek tipleme ile EAFP)
def toplam(kaynak):
try:
return sum(kaynak.values()) # sözlük mü?
except AttributeError:
return sum(kaynak) # değilse liste gibi davran
print(toplam({"a": 1, "b": 2}), toplam([3, 4]))
LBYL'in haklı olduğu yerler: kontrol ucuzsa ve hata yolu pahalıysa (dosya silmeden önce var mı diye bakmak), ya da istisna çok sık oluşuyorsa (istisna yakalamak başarılı yoldan daha yavaştır).
assert'in yeri
assert koşul, "mesaj" koşul yanlışsa AssertionError yükseltir. Amacı programcı varsayımlarını geliştirme sırasında doğrulamaktır: "bu noktada liste boş olamaz". Kullanıcı girdisi, dış veri ya da iş kuralı doğrulamak için kullanılmaz, çünkü Python -O bayrağıyla çalıştırıldığında tüm assert'ler silinir ve kontroller yok olur.
def ortalama(tutarlar):
assert isinstance(tutarlar, list), "iç kullanım: liste bekleniyor" # programcı varsayımı
if not tutarlar: # iş kuralı: gerçek istisna
raise ValueError("boş liste için ortalama tanımsız")
return sum(tutarlar) / len(tutarlar)
print(ortalama([10, 20]))
try:
ortalama([])
except ValueError as e:
print("ValueError:", e)
try:
ortalama("abc")
except AssertionError as e:
print("AssertionError:", e)
Derinleş: zincirler, notlar, gruplar ve sınırlar
Hiyerarşi kurmayı, raise from'u ve loglamayı gördün. Bu bölümde gerçek sistemlerde fark yaratan dört konuya iniyoruz: iz kaydındaki iki farklı zincir cümlesini okumak, hataya bağlam notu eklemek (add_note), birden çok hatayı birlikte taşımak (ExceptionGroup ve except*) ve hataları sistem sınırında (API, CLI) anlamlı yanıtlara çevirmek. Sonunda finally tuzaklarını ve warnings modülünü ekliyoruz. Logger yapılandırmasının ayrıntısı "logging" dersinde, with ve bağlam yöneticisi (context manager) yazmak "Dekoratörler (decorator)" dersinde.
1. İki zincir cümlesi: cause ve context
İz kaydında iki farklı cümle görebilirsin ve anlamları farklıdır:
| İz kaydındaki cümle | Nasıl oluşur | Nitelik | Anlamı |
|---|---|---|---|
| The above exception was the direct cause of the following exception | raise B from a | __cause__ | Bilinçli çeviri: "B'nin nedeni a" |
| During handling of the above exception, another exception occurred | except içinde from'suz yeni hata | __context__ | Çoğunlukla kaza: hata işlenirken ikinci hata çıktı |
| (yalnız son hata) | raise B from None | __suppress_context__ = True | Nedeni bilinçli gizle |
"During handling..." cümlesini görürsen önce except bloğunun içine bak: çoğu zaman log mesajını biçimlerken ya da temizlik yaparken yeni bir hata çıkmıştır.
def kur_al(kurlar, birim):
try:
return kurlar[birim]
except KeyError:
print("bilinmeyen birim: " + birim.upper()) # birim None ise burada 2. hata!
try:
kur_al({"USD": 32.5}, None)
except AttributeError as e:
print(type(e).__name__, "| context:", type(e.__context__).__name__, "| cause:", e.__cause__)
2. add_note: hatayı değiştirmeden bağlam eklemek
Python 3.11 ile her istisnaya e.add_note("...") ile not eklenebilir; notlar iz kaydının altında görünür ve e.__notes__ listesinde durur. Asıl farkı: hatanın türünü değiştirmezsin. Çağıran hâlâ ValueError yakalar ama ekstra bilgi (hangi satır, hangi dosya, hangi müşteri) kaybolmaz. Yeni istisna sınıfına çevirmek (raise from) ile not eklemek arasındaki seçim şudur: çağıranın tepkisi değişecekse çevir, yalnız teşhis bilgisi ekliyorsan not düş.
satirlar = ["Ayşe;1200", "Can;12OO", "Ece;300"]
def toplam(satirlar):
t = 0
for no, satir in enumerate(satirlar, start=1):
try:
ad, tutar = satir.split(";")
t += int(tutar)
except ValueError as e:
e.add_note(f"satır {no}: {satir!r}")
raise # aynı hata, ek notla
return t
try:
toplam(satirlar)
except ValueError as e:
print("Hata:", e)
print("Notlar:", e.__notes__)
3. ExceptionGroup ve except*: hepsini birden bildir
Kredi başvuru formunda üç alan hatalıysa kullanıcıyı üç kez geri göndermek kötü deneyimdir. Tüm hataları bir listede toplayıp sonunda raise ExceptionGroup("mesaj", hatalar) yaparsın. Yakalayan taraf except* ile gruptan türe göre parça seçer: her except* bloğu kendi türündeki hataları alt grup olarak alır ve birden çok blok aynı grup için çalışabilir.
def basvuru_dogrula(b):
hatalar = []
if b.get("yas", 0) < 18:
hatalar.append(ValueError(f"yaş 18'den küçük: {b.get('yas')}"))
if not str(b.get("tckn", "")).isdigit():
hatalar.append(ValueError("TCKN yalnız rakam olmalı"))
if not isinstance(b.get("gelir"), (int, float)):
hatalar.append(TypeError("gelir sayı olmalı"))
if hatalar:
raise ExceptionGroup("başvuru geçersiz", hatalar)
return True
try:
basvuru_dogrula({"yas": 16, "tckn": "12a", "gelir": "yok"})
except* ValueError as g:
for h in g.exceptions:
print("Değer hatası:", h)
except* TypeError as g:
print("Tür hatası:", [str(h) for h in g.exceptions])
except* ile düz except aynı try'da karıştırılamaz. Hiçbir except* bloğunun yakalamadığı parçalar yeni bir grup olarak yukarı fırlatılır. Grubu düz except ExceptionGroup as g ile de yakalayıp g.exceptions'ı dolaşabilirsin (testlerde pratik).4. Sınırda çeviri: hata → kullanıcı yanıtı
Bir web servisinde iç katmanlar anlamlı istisnalar fırlatır; en dış katman (istek işleyicisi) bunları HTTP durum koduna ve kullanıcıya gösterilecek kısa koda çevirir. Bu eşlemeyi istisna sınıflarına sınıf niteliği olarak koymak, devasa bir if/elif zincirini ortadan kaldırır. Beklenmeyen her şey 500 olur, ayrıntısı yalnız loga gider.
class UygulamaHatasi(Exception):
http, kod = 400, "GENEL"
class Bulunamadi(UygulamaHatasi):
http, kod = 404, "BULUNAMADI"
class YetkiYok(UygulamaHatasi):
http, kod = 403, "YETKI_YOK"
def yanitla(f, *args):
try:
return 200, f(*args)
except UygulamaHatasi as e:
return e.http, {"kod": e.kod, "mesaj": str(e)}
except Exception:
# gerçek kodda: log.exception("beklenmeyen hata")
return 500, {"kod": "IC_HATA", "mesaj": "beklenmeyen bir hata oluştu"}
def hesap_getir(no):
if no == "TR00":
raise YetkiYok("bu hesaba erişim yetkiniz yok")
if no != "TR01":
raise Bulunamadi(f"hesap yok: {no}")
return {"no": no, "bakiye": 1500}
for no in ["TR01", "TR09", "TR00", None]:
print(yanitla(hesap_getir, no) if no else yanitla(lambda: 1 / 0))
5. finally tuzakları ve else'in yeri
finally her durumda çalışır; bu yüzden içine return, break ya da continue yazarsan fırlatılmakta olan istisnayı sessizce yutar. Python 3.14 bunun için uyarı vermeye başladı; kural basit: finally yalnız temizlik yapar (dosya kapat, kilidi bırak), sonuç döndürmez. else bloğu ise "try başarılı olduysa" çalışır; yalnız hata çıkarabilecek satırı try'da tutup gerisini else'e koymak, yanlışlıkla başka bir satırın hatasını yakalamanı engeller.
6. warnings: hata değil, uyarı
Bazı durumlar programı durdurmayı gerektirmez ama bilinmelidir: eskiyen (deprecated) bir fonksiyonun kullanılması, şüpheli ama işlenebilen veri. warnings.warn("mesaj", DeprecationWarning, stacklevel=2) bunu yapar; stacklevel=2 uyarıyı çağıranın satırında gösterir. Testlerde warnings.catch_warnings(record=True) ile uyarıları yakalayıp doğrularsın; python -W error ile çalıştırınca uyarılar hataya dönüşür (CI'da yaygın).
import warnings
def cevir(tutar, kur):
return round(tutar / kur, 2)
def eski_cevir(tutar):
warnings.warn("eski_cevir kaldırılacak; cevir(tutar, kur) kullanın",
DeprecationWarning, stacklevel=2)
return cevir(tutar, 32.5)
with warnings.catch_warnings(record=True) as kayit:
warnings.simplefilter("always")
print(eski_cevir(650))
print(len(kayit), kayit[0].category.__name__, "-", kayit[0].message)
from ile çevirir; yalnız sınır katmanı (API/CLI) HTTP koduna ya da çıkış koduna çevirir; beklenmeyen her hata tam iz kaydıyla loglanır ve kullanıcıya iç ayrıntı sızmaz." Toplu veri yüklemelerde de iki mod bulunur: katı mod ilk grupta durur (ExceptionGroup), esnek mod hatalı satırları rapora yazıp geçerlileri işler. Hangi modun çalıştığı her zaman loglanır.İstisna gruplarıyla toplu doğrulama (3.11+)
Bir formda beş alan hatalıysa kullanıcıya ilkini değil hepsini göstermek istersin. ExceptionGroup("doğrulama", [h1, h2, ...]) birden çok istisnayı tek nesnede taşır; except* sözdizimi gruptan türe göre seçer. Toplu veri yükleme ve eşzamanlı görevlerde (asyncio TaskGroup) standart araçtır.
Hata mesajı yazma kuralları
- Ne bekleniyordu, ne geldi:
"tutar pozitif olmalı, gelen: -5". - Bağlam ekle: hangi hesap, hangi satır, hangi dosya.
- Çözümü ima et:
"hesap bulunamadı: TR09 (numara TR ile başlamalı)". - Kullanıcıya giden mesajla loga giden mesajı ayır: iç detay (SQL, dosya yolu) loga, sade cümle kullanıcıya.
Logger hiyerarşisi
getLogger("anadolu.transfer") adı noktayla ayrılmış bir ağaçtır: anadolu logger'ının seviyesi ve işleyicileri alt loggerlara miras geçer. Modül başında log = logging.getLogger(__name__) yazmak standarttır; uygulama girişinde bir kez basicConfig ya da dictConfig ile yapılandırma yapılır, kütüphane kodu asla basicConfig çağırmaz.
Hata yönetimi stratejisi
Her katman yalnızca anlamlı tepki verebildiği hatayı yakalar; gerisini yukarı bırakır. Hatayı en altta yakalayıp None döndürmek sorunu ertelemektir: "None nereden geldi?" sorusu üç katman yukarıda, saatler sonra sorulur. En dış katmanda (CLI giriş noktası, web isteği işleyicisi) tek bir except BankaHatasi kullanıcı mesajını, tek bir except Exception ile log.exception beklenmeyeni raporlar.