Doğru Görünen Kod, Yanlış Sonuç: Python’da with ve sqlite3 Tuzağı

Yapay zekânın yazdığı düzgün görünen bir sqlite3 kodu, hata mesajı vermeden yanlış sonuç üretti. Suçlu ne kod ne SQLite çıktı: Türkçe yerel ayarda Python 3.12, INSERT komutunu tanımıyor. Sorunu adım adım nasıl ayırdığımızı ve autocommit=False çözümünü anlatıyoruz.

Bu yazının örnek kodunu bir yapay zekâ (Claude) yazdı. Kod düzgündü, Python belgelerine de uyuyordu. Tek bir sorunu vardı: bizim bilgisayarımızda beklenen sonucu vermedi. Hiçbir hata mesajı da çıkmadı. Program sessizce, olmaması gereken bir kaydı veritabanında bıraktı. Sorunun kaynağı ne koddu ne de SQLite. Kaynak, bilgisayarın Türkçe olarak ayarlanmış olmasıydı. Bu yazıda o kodu, sorunu nasıl fark ettiğimizi, ilk tahminimizin neden yanlış çıktığını ve çözümü anlatıyoruz. Yol üzerinde Python’un with deyiminin nasıl çalıştığını da göreceğiz.

Yapay Zekâ Kod Yazıyorsa Neyi Öğrenmemiz Gerekiyor?

Kod yazmanın büyük kısmını artık yapay zekâ araçlarına bırakabiliyoruz. Bu yazıdaki örnekler de birkaç saniyede üretildi. Ama üretilen kodun doğru çalışıp çalışmadığına hâlâ bir insanın karar vermesi gerekiyor. Bunun için kodun ne yapması gerektiğini bilmek, onu gerçekten çalıştırmak ve sonucu dikkatle okumak gerekiyor.

Aşağıdaki hikâye bunun küçük ama öğretici bir örneği. Konumuz, Python’da kaynakları güvenle yönetmek için kullanılan with deyimi ve onun SQLite veritabanıyla birlikte kullanımı.

Proje bilgisi Yazarlar: Ahmet Aksoy & Claude · Tarih: 08.10.2026 Ortamlar: Python 3.12.3 (Ubuntu 24.04, SQLite 3.45.1, yerel ayar tr_TR.UTF-8) ve Docker’da Python 3.11–3.14 resmi imajları (SQLite 3.46.1)

Önce Kısaca: with Deyimi Ne Yapar?

Bir dosyayı ya da veritabanı bağlantısını açtığınızda, iş bitince onu kapatmanız gerekir. Araya bir hata girerse kapatma satırı hiç çalışmayabilir. with deyimi bunu garanti altına alır:

with open(path, "w", encoding="utf-8") as f:
    f.write("ilk satır\n")
    raise ValueError("yazarken bir şey ters gitti")

# Hata yukarı iletilir, ama dosya yine de kapanmıştır:
# dosya kapalı mı? True

with ile kullanılabilen nesnelere context manager (bağlam yöneticisi) denir. Bu nesnelerin iki özel metodu vardır. Blok başlarken __enter__, blok biterken __exit__ çağrılır. __exit__ blok hatayla bitse de çağrılır ve hatanın ne olduğunu parametre olarak alır. Kendi context manager’ınızı yazmak için bu iki metodu tanımlamanız yeterli:

class Timer:
    def __enter__(self):
        self.start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc, tb):
        self.elapsed = (time.perf_counter() - self.start) * 1000
        print(f"{self.elapsed:.1f} ms, {'hata ile' if exc_type else 'sorunsuz'} bitti")
        return False   # hatayı yutma, yukarı ilet

SQLite bağlantısı da bir context manager. Ancak with conn: çoğu kişinin sandığı gibi bağlantıyı kapatmaz. Yalnızca işlemi (transaction) yönetir: blok sorunsuz biterse değişiklikleri kaydeder (commit), hatayla biterse hepsini geri alır (rollback). Sorunumuz tam olarak bu geri alma adımında çıktı.

Kod: Ya Hepsi Ya Hiçbiri

Örnekte iki kitap kaydını tek bir işlemde eklemeye çalışıyoruz. İkinci kayıt, sayfa sayısı negatif olduğu için veritabanının CHECK kuralına takılıyor. Beklenen davranış şu: hata olunca blok içindeki iki kayıt da geri alınmalı. İşlemlerin amacı zaten bu.

conn = sqlite3.connect(db)
conn.execute("CREATE TABLE kitap (ad TEXT NOT NULL, sayfa INTEGER CHECK (sayfa > 0))")

with conn:
    conn.execute("INSERT INTO kitap VALUES (?, ?)", ("Kuyucaklı Yusuf", 230))
try:
    with conn:
        conn.execute("INSERT INTO kitap VALUES (?, ?)", ("Sinekli Bakkal", 348))
        conn.execute("INSERT INTO kitap VALUES (?, ?)", ("Hatalı Kayıt", -5))
except sqlite3.IntegrityError as exc:
    print("hata         :", exc)
print("kayıtlar     :", [r[0] for r in conn.execute("SELECT ad FROM kitap")])

Kodu çalıştırdığımızda şu çıktıyı aldık:

hata         : CHECK constraint failed: sayfa > 0
kayıtlar     : ['Kuyucaklı Yusuf', 'Sinekli Bakkal']

Hata beklendiği gibi yakalandı. Ama “Sinekli Bakkal” tabloda kalmıştı, yani işlem geri alınmamıştı. Program hiçbir uyarı vermedi. Sonuç listesini okumasaydık bu durumu fark etmeyecektik.

Sorun Kodda mı, Ortamda mı?

Bir yapay zekânın yazdığı kod beklenmedik sonuç verdiğinde ilk soru şu olmalı: Kod mu yanlış, yoksa bizim beklentimiz mi? Bunu ayırt etmek için adım adım ilerledik.

1. İşlem gerçekten açılıyor mu? sqlite3 bağlantısının in_transaction özelliği, o anda açık bir işlem olup olmadığını gösterir. INSERT komutundan hemen sonra bu değere baktık:

conn.execute("INSERT INTO kitap VALUES ('B', 2)")
print("in_transaction:", conn.in_transaction)

# Çıktı (bizim bilgisayarımızda):
# in_transaction: False

Sorunun kaynağı buydu. Python belgelerine göre sqlite3 modülü, varsayılan modunda INSERT gibi değişiklik yapan komutlardan önce işlemi kendisi başlatmalı. Burada başlatmamıştı. Açık bir işlem olmayınca her komut anında kaydediliyor ve geri alınacak bir şey kalmıyordu.

2. Bilgisayardaki bir ayar mı? Python’un kullanıcıya özel ayarları ve ortam değişkenleri davranışı değiştirebilir. Betiği bunların hiçbirini yüklemeyen yalıtılmış modda (python3 -I) çalıştırdık. Sonuç değişmedi.

3. Başka bir ortamda da aynı mı? Aynı betiği resmi Python Docker imajında çalıştırdık:

Ortam Python SQLite Yerel ayar in_transaction Geri alma
Ubuntu 24.04 paketi 3.12.3 3.45.1 tr_TR.UTF-8 False Çalışmadı
Docker python:3.12-slim 3.12.15 3.46.1 C.UTF-8 True Çalıştı

Kod aynıydı, sonuç farklıydı. Demek ki sorun kodda değil ortamdaydı. Tabloya bakınca ilk tahminimiz, Ubuntu’nun Python paketi ile SQLite sürümünün birleşiminde bir sorun olduğuydu. Bu yazının ilk taslağında da bunu yazmıştık.

Ama “ortam” tek bir şey değil. İki ortam arasında Python sürümü, SQLite sürümü ve Ubuntu’nun yamaları dışında bir fark daha vardı: yerel ayar (locale). Bizim bilgisayarımız Türkçe kurulu, yerel ayarı tr_TR.UTF-8. Docker imajları ise varsayılan olarak C.UTF-8 ile çalışıyor. 2. adımda kullandığımız -I seçeneği de bu farkı ortadan kaldırmıyordu. -I yalnızca Python’un kendi kullanıcı ayarlarını ve PYTHON ile başlayan ortam değişkenlerini devre dışı bırakır. Sistemin yerel ayarına dokunmaz.

4. Farkı tek tek deneyelim. Önce aynı bilgisayarda, aynı Python ve aynı SQLite ile yalnızca yerel ayarı değiştirdik. INSERT, UPDATE ve DELETE komutlarından sonra in_transaction değerine baktık. Küçük harfle yazılmış insert komutunu da ayrıca denedik:

LC_ALL=tr_TR.UTF-8 python3 deneme.py
LC_ALL=C.UTF-8 python3 deneme.py
Yerel ayar INSERT insert UPDATE DELETE
tr_TR.UTF-8 False True True True
C.UTF-8 True True True True
en_US.UTF-8 True True True True

Sorunu yalnızca yerel ayar belirliyordu. Ardından ters yönden de denedik ve Docker imajlarına Türkçe yerel ayarı kurduk:

Python (Docker) tr_TR.UTF-8 ile INSERT C.UTF-8 ile INSERT
3.11.17 False True
3.12.15 False True
3.13.16 True True
3.14.8 True True

Docker’daki 3.12 de Türkçe yerel ayarla aynı hatayı verdi. Yani Ubuntu’nun bu işte bir suçu yoktu. Sorun Python 3.13 ile ortadan kalkıyordu.

5. Neden yalnızca INSERT, neden yalnızca Türkçe? sqlite3 modülü varsayılan (eski) modunda bir işlemi açıp açmayacağına komutun ilk kelimesine bakarak karar veriyor. Komut insert, update, delete ya da replace ile başlıyorsa önce BEGIN gönderiyor. Bu karşılaştırma büyük/küçük harfe duyarsız yapılıyor. Python 3.12’de bunun için C dilinin yerel ayara bağlı tolower() fonksiyonu kullanılıyordu.

Türkçede I harfinin küçüğü i değil, ı. Bu yüzden Türkçe yerel ayarda tolower('I') sonucu i olmuyor: ı tek baytlık bir karakter olmadığı için I olduğu gibi kalıyor. Böylece INSERT kelimesi insert ile eşleşmiyor, Python işlemi açmıyor ve her komut anında kaydediliyor. UPDATE, DELETE ve REPLACE kelimelerinde I harfi olmadığı için onlar etkilenmiyor. Yazılımcıların “Türkçe I sorunu” dediği bilinen bir tuzak bu.

Python 3.13’te bu karşılaştırmalar yerel ayardan bağımsız, yalnızca ASCII harflerle çalışan sürümleriyle değiştirildi (gh-108767). Değişiklik 3.12’ye taşınmadı. Bu yüzden Türkçe kurulu bir Linux’ta Python 3.11 ya da 3.12 kullanan herkes bu tuzağa düşebilir.

6. Belgeler ne öneriyor? Python 3.12 ile sqlite3’e yeni bir parametre eklendi: autocommit. Bu parametre False olduğunda Python, işlemin açılıp açılmayacağına komutun türüne bakarak karar vermiyor. İşlemi bağlantı açılır açılmaz ve her commit ya da rollback sonrasında kendisi başlatıyor.

Çözüm: autocommit=False

Koddaki tek değişiklik bağlantıyı açtığımız satır oldu:

conn = sqlite3.connect(db, autocommit=False)

Sonuçlar:

Ortam Varsayılan mod autocommit=False
tr_TR.UTF-8 (3.12.3) ['Sinekli Bakkal'] kaldı Tablo temiz
C.UTF-8 (3.12.3) Tablo temiz Tablo temiz

İki yerel ayar artık aynı davranıyor. Yeni yazdığınız veritabanı kodunda bu parametreyi kullanmanızı öneririz. Python 3.11 veya daha eski bir sürüme bağlıysanız, with conn: bloğunun ilk satırında işlemi conn.execute("BEGIN") ile kendiniz başlatın. SQL komutlarını küçük harfle (insert into ...) yazmak da sorunu önlüyor, ama bu kolayca unutulabilecek bir önlem. Türkçe yerel ayarda üç yöntemi de denedik, üçünde de geri alma doğru çalıştı. Python 3.13’e geçmek de sorunu ortadan kaldırıyor.

Bağlantının kendisini de with ile kapatmak isterseniz contextlib.closing kullanabilirsiniz. Aynı satırda iki context manager yan yana yazılabiliyor ve ters sırayla kapanıyor: önce işlem kaydediliyor, sonra bağlantı kapanıyor.

with contextlib.closing(sqlite3.connect(db, autocommit=False)) as conn, conn:
    conn.execute("INSERT INTO kitap VALUES (?, ?)", ("Çalıkuşu", 420))

Bu Hikâyeden Ne Çıkardık?

  • Hata mesajı olmaması, kodun doğru çalıştığı anlamına gelmiyor. Buradaki sorun hiçbir uyarı vermedi. Fark etmemizi sağlayan, çıktıdaki kayıt listesini gerçekten okumamız oldu.
  • Kodu çalıştırın, sonucu okuyun. Yapay zekânın yazdığı kod düzgün görünür ve çoğu zaman da doğrudur. Ama “doğru görünüyor” ile “doğru çalışıyor” arasındaki farkı ancak çalıştırarak görebilirsiniz.
  • Durumu sorgulayın, varsaymayın. in_transaction gibi durum bilgileri, programın içinde gerçekte ne olduğunu gösterir. Sorunun yerini tek satırla bulmamızı sağlayan buydu.
  • Farklı bir ortamda deneyin. Docker ile resmi bir imajda çalıştırmak, “kodum mu yanlış, ortamım mı?” sorusunu dakikalar içinde cevapladı.
  • “Ortam” derken neyin değiştiğini tek tek ayırın. Docker sorunun ortamda olduğunu gösterdi ama ortamın hangi parçasında olduğunu göstermedi. İlk tahminimiz (Ubuntu paketi) akla yatkındı ama yanlıştı. Doğru cevabı, her seferinde tek bir şeyi (burada yerel ayarı) değiştirerek bulduk.
  • Belirsizliği koddan kaldırın. Varsayılan davranışa güvenmek yerine autocommit=False ile ne istediğimizi açıkça yazdık. Kod artık ortamdan bağımsız olarak aynı davranıyor.

Bu dersleri uygulayabilmek için yine de yazılım bilgisi gerekiyor: işlemin (transaction) ne olduğunu, with deyiminin ne yaptığını ve sonucun nasıl görünmesi gerektiğini bilmek. Değişen şey, bu bilgiyi kod yazmaktan çok kodu değerlendirmek için kullanmamız.

Kodu Kendiniz Deneyin

Yazıdaki örneklerin tamamı, with deyiminin diğer kullanımlarıyla birlikte tek bir main.py dosyasında. Bu diğer kullanımlar şunlar: @contextmanager ile üreteç tabanlı context manager, belirli bir hatayı bilerek yutan contextlib.suppress ve sayısı önceden bilinmeyen dosyaları yöneten ExitStack. Betik Python 3.12 veya daha yenisini gerektiriyor ve bütün dosyalarını geçici bir klasörde oluşturuyor:

python3 main.py

Bilgisayarınızın yerel ayarı Türkçe ise (locale komutunun çıktısında LC_CTYPE="tr_TR.UTF-8" görünüyorsa) ve Python 3.12 kullanıyorsanız, sorunu kendi bilgisayarınızda görebilirsiniz. Bunun için main.py içindeki autocommit=False parametresini silip betiği yeniden çalıştırmanız yeterli. Yerel ayarınız Türkçe değilse ama sistemde Türkçe yerel ayar kuruluysa betiği LC_ALL=tr_TR.UTF-8 python3 main.py komutuyla çalıştırabilirsiniz.

Kaynaklar

Ahmet Aksoy & Claude

Not: Bu yazıda incelediğimiz kodu ve benzer projelerin kaynak kodlarını https://github.com/ahmetax/practical-python-examples adresinde bulabilirsiniz.

Bir Yanıt Bırak

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir