Python'da with deyimi, bir kaynağın kullanım sonunda mutlaka serbest bırakılmasını sağlar: with open("veri.txt", encoding="utf-8") as f: yazdığınızda blok ister normal biter, ister bir istisnayla kesilir, dosya her durumda kapanır. Bu işi yapan nesneye bağlam yöneticisi (context manager) denir. Bir sınıfa __enter__() ve __exit__() metotlarını ekleyerek ya da contextlib.contextmanager dekoratörüyle tek bir üretici fonksiyon yazarak kendi bağlam yöneticinizi oluşturabilirsiniz.

Bu yazıda with deyiminin adım adım nasıl çalıştığını ve hangi try/finally yapısına denk geldiğini, sınıf tabanlı bağlam yöneticisi yazmayı, __exit__() metodunun hataları nasıl bastırabildiğini, birden fazla kaynağı tek satırda açmayı, @contextmanager kullanımını ve contextlib modülündeki suppress, closing, nullcontext, redirect_stdout, chdir ve ExitStack araçlarını Python 3.15.2'de çalıştırılmış örneklerle anlatıyoruz. Hata yakalamanın temeli için Python try except, dosya işlemleri için dosya okuma ve yazma yazılarına, tüm konular için Python öğrenme rehberi sayfasına bakabilirsiniz.

with deyimi neyi çözer?

with deyimi neyi çözer?

Dosyalar, ağ bağlantıları, kilitler ve geçici değişiklikler ortak bir sorun taşır: bir yerde açılır ya da değiştirilir, başka bir yerde kapatılmalı ya da eski hâline getirilmelidir. Kapatma kodunu normal akışa yazarsanız, aradaki bir istisna o satıra hiç ulaşılmamasına yol açar. Resmî öğretici bunu for line in open("dosya.txt") örneğiyle anlatır: bu yazımda dosya, kod bittikten sonra belirsiz bir süre açık kalabilir. Klasik çözüm try/finally yazmaktır; with ise aynı güvenceyi daha kısa ve tekrar kullanılabilir biçimde sağlar:

python
with open("sayilar.txt", "w", encoding="utf-8") as f:
    f.write("1\n2\nüç\n")

toplam = 0
try:
    with open("sayilar.txt", encoding="utf-8") as f:
        for satir in f:
            toplam += int(satir)
except ValueError as e:
    print("Hata:", e)
print("Toplam:", toplam, "| dosya kapalı mı:", f.closed)
# çıktı: Hata: invalid literal for int() with base 10: 'üç\n'
# çıktı: Toplam: 3 | dosya kapalı mı: True

Üçüncü satırda int() hata verdi ve döngü yarıda kesildi, ama dosya yine de kapandı. Hata mesajında 'üç\n' görünmesinin nedeni satır sonu karakteridir; int() baştaki ve sondaki boşlukları kabul ettiği için 1 ve 2 sorunsuz dönüştü.

with deyimi adım adım nasıl çalışır?

Dil referansı, tek öğeli bir with deyiminin yürütülmesini şöyle tanımlar: önce bağlam ifadesi (with sözcüğünden sonraki ifade) değerlendirilip bağlam yöneticisi elde edilir. Sonra yöneticinin __enter__() ve __exit__() metotları sonradan kullanılmak üzere alınır ve __enter__() çağrılır. as yazılmışsa __enter__() dönüş değeri o hedefe atanır. Ardından blok çalışır ve en sonda __exit__() çağrılır. Blok bir istisnayla terk edildiyse __exit__() metoduna istisnanın türü, değeri ve traceback'i verilir; aksi hâlde üç argüman da None olur. Referans, bu davranışın aşağı yukarı şu koda denk geldiğini gösterir:

python
# with IFADE as HEDEF:
#     BLOK
# deyiminin yaklaşık karşılığı:
manager = (IFADE)
enter = manager.__enter__
exit = manager.__exit__
value = enter()
hit_except = False

try:
    HEDEF = value
    BLOK
except:
    hit_except = True
    if not exit(*sys.exc_info()):
        raise
finally:
    if not hit_except:
        exit(None, None, None)

Buradan iki önemli kural çıkar. Birincisi, __enter__() hatasız dönerse __exit__() her durumda çağrılır; as hedefine atama sırasında çıkan bir hata bile bloğun içindeki hata gibi ele alınır. İkincisi, blok bir istisnayla bittiyse ve __exit__() yanlış (false) bir değer döndürürse istisna yeniden fırlatılır; doğru (true) bir değer döndürürse istisna bastırılır ve program with deyiminden sonraki satırdan devam eder. Blok istisna dışında bir nedenle (normal bitiş, return, break) terk edildiyse dönüş değeri yok sayılır.

Sınıf ile bağlam yöneticisi yazmak

Bir nesnenin with ile kullanılabilmesi için sınıfında __enter__(self) ve __exit__(self, exc_type, exc_value, traceback) metotları tanımlı olmalıdır. object sınıfının kendisi bu metotları sağlamaz. Aşağıdaki Baglanti sınıfı açılışta ve kapanışta mesaj yazıyor ve __exit__() içinde istisna olup olmadığını raporluyor:

python
class Baglanti:
    def __init__(self, ad):
        self.ad = ad

    def __enter__(self):
        print(f"{self.ad}: açıldı")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        tur = exc_type.__name__ if exc_type else None
        print(f"{self.ad}: kapandı (hata: {tur})")
        return False

    def sorgu(self, metin):
        return f"{metin!r} çalıştırıldı"

with Baglanti("veritabanı") as b:
    print(b.sorgu("SELECT 1"))

try:
    with Baglanti("önbellek"):
        raise ValueError("bozuk veri")
except ValueError as e:
    print("dışarıda yakalandı:", e)
# çıktı: veritabanı: açıldı
# çıktı: 'SELECT 1' çalıştırıldı
# çıktı: veritabanı: kapandı (hata: None)
# çıktı: önbellek: açıldı
# çıktı: önbellek: kapandı (hata: ValueError)
# çıktı: dışarıda yakalandı: bozuk veri

__exit__() False döndürdüğü için ValueError bastırılmadı ve dıştaki try onu yakaladı. Veri modeli belgesi bir ayrıntının altını çizer: __exit__() kendisine verilen istisnayı yeniden fırlatmamalıdır; istisnanın yayılmasına izin vermek için yanlış bir değer döndürmek yeterlidir, yeniden fırlatma çağıranın sorumluluğundadır. __enter__()'in self döndürmesi de bir zorunluluk değildir; as hedefine hangi nesnenin bağlanacağına siz karar verirsiniz. Sınıflar ve özel metotlar hakkında ayrıntılı bilgi için Python dunder metotları yazısına bakabilirsiniz.

with sözcüğünden sonra bağlam yöneticisi olmayan bir nesne yazarsanız Python 3.15.2 hatanın nedenini açıkça söyler:

python
try:
    with 5:
        pass
except TypeError as e:
    print(e)
# çıktı: 'int' object does not support the context manager protocol (missed __exit__ method)

__exit__ ile istisna bastırmak

__exit__ ile istisna bastırmak

__exit__() metodundan True döndürmek, bloktaki istisnayı yutar. Bu güçlü ama tehlikeli bir özelliktir; yalnızca belirli ve beklenen hata türleri için kullanılmalıdır. Aşağıdaki sınıf kendisine verilen türleri bastırıyor ve bastırdığı hatayı kaydediyor:

python
class Yut:
    def __init__(self, *turler):
        self.turler = turler
        self.yakalanan = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is not None and issubclass(exc_type, self.turler):
            self.yakalanan = exc_value
            return True
        return False

with Yut(KeyError) as y:
    {}["eksik"]
    print("bu satır çalışmaz")
print("devam ediliyor, yakalanan:", repr(y.yakalanan))
# çıktı: devam ediliyor, yakalanan: KeyError('eksik')

Blok, istisnanın oluştuğu satırda bitti ve program with deyiminden sonra devam etti. Standart kütüphane bu işi zaten contextlib.suppress() ile yapar; kendi sınıfınızı yazmanız genellikle gerekmez.

Birden fazla bağlam yöneticisi

Tek bir with satırına virgülle ayrılmış birden fazla öğe yazabilirsiniz; bu, iç içe yazılmış with deyimleriyle tamamen aynıdır. Python 3.10'dan beri öğeleri parantez içine alıp birden fazla satıra bölmek de mümkündür. Kapanış sırası açılış sırasının tersidir:

python
class Baglanti:
    def __init__(self, ad):
        self.ad = ad

    def __enter__(self):
        print(f"{self.ad}: açıldı")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        tur = exc_type.__name__ if exc_type else None
        print(f"{self.ad}: kapandı (hata: {tur})")
        return False

    def sorgu(self, metin):
        return f"{metin!r} çalıştırıldı"

with (
    Baglanti("A") as a,
    Baglanti("B") as b,
):
    print("ikisi de açık")
# çıktı: A: açıldı
# çıktı: B: açıldı
# çıktı: ikisi de açık
# çıktı: B: kapandı (hata: None)
# çıktı: A: kapandı (hata: None)

Bir dosyadan okuyup diğerine yazmak bu yazımın en yaygın kullanımıdır: with open("girdi.txt", encoding="utf-8") as kaynak, open("cikti.txt", "w", encoding="utf-8") as hedef:. İkinci dosya açılırken hata çıkarsa birincisi yine kapanır.

@contextmanager: üretici fonksiyonla bağlam yöneticisi

Her seferinde sınıf yazmak yerine contextlib.contextmanager dekoratörünü kullanabilirsiniz. Dekore edilen fonksiyon bir üretici (generator) olmalı ve tam olarak bir kez yield etmelidir. yield öncesindeki kod __enter__(), sonrası __exit__() görevini görür; yield edilen değer as hedefine bağlanır. Bloğun içinde bir istisna oluşursa Python onu üreticinin içinde, yield satırında yeniden fırlatır. Bu yüzden temizlik kodunu try/finally içine almak şarttır:

python
from contextlib import contextmanager

ayarlar = {"hata_ayiklama": False}

@contextmanager
def gecici_ayar(anahtar, deger):
    eski = ayarlar[anahtar]
    ayarlar[anahtar] = deger
    try:
        yield ayarlar
    finally:
        ayarlar[anahtar] = eski

with gecici_ayar("hata_ayiklama", True) as a:
    print("içeride:", a["hata_ayiklama"])
print("dışarıda:", ayarlar["hata_ayiklama"])

try:
    with gecici_ayar("hata_ayiklama", True):
        raise RuntimeError("test")
except RuntimeError:
    pass
print("hatadan sonra:", ayarlar["hata_ayiklama"])
# çıktı: içeride: True
# çıktı: dışarıda: False
# çıktı: hatadan sonra: False

try/finally olmasaydı ikinci with bloğundaki RuntimeError üreticinin yield satırında fırlatılır, ayarlar[anahtar] = eski satırı hiç çalışmaz ve ayar True olarak kalırdı. Belgeler bir kuralı daha hatırlatır: üretici istisnayı yalnızca kaydetmek için yakalıyorsa onu yeniden fırlatmalıdır; aksi hâlde with deyimine istisnanın ele alındığı bildirilir ve hata bastırılmış olur.

@contextmanager ile oluşturulan nesneler tek kullanımlıktır: her with için fonksiyonu yeniden çağırmanız gerekir. Belgeler bu yüzden bağlam yöneticilerinin doğrudan with satırında oluşturulmasını önerir. Aynı nesneyi ikinci kez kullanmayı denediğimizde Python 3.15.2 bir hata verdi; belgelerdeki örnek bu durumda RuntimeError: generator didn't yield gösterse de bizim denememizde çıkan hata farklıydı:

python
from contextlib import contextmanager

@contextmanager
def tek_seferlik():
    print("önce")
    yield
    print("sonra")

cm = tek_seferlik()
with cm:
    pass
try:
    with cm:
        pass
except Exception as e:
    print(type(e).__name__, "-", e)
# çıktı: önce
# çıktı: sonra
# çıktı: AttributeError - '_GeneratorContextManager' object has no attribute 'args'

Hangi hatayı alırsanız alın ders aynıdır: cm = tek_seferlik() gibi bir nesneyi saklayıp yeniden kullanmayın, with tek_seferlik(): yazın. Üreticilerin nasıl çalıştığını ayrıntılı öğrenmek için Python generator ve yield yazısına bakabilirsiniz.

Dekoratör olarak kullanmak ve Python 3.15 değişikliği

@contextmanager ile yazılan yöneticiler ContextDecorator tabanlıdır, yani bir fonksiyonun tamamını sarmak için dekoratör olarak da kullanılabilir. Her çağrıda yeni bir üretici örneği oluşturulduğu için tek kullanımlık olma sorunu bu kullanımda ortaya çıkmaz. Python 3.15 burada önemli bir davranışı düzeltti: önceden bir üretici fonksiyonu ya da eşyordam (coroutine) fonksiyonu bu şekilde dekore edildiğinde bağlam, üretici nesnesi oluşturulur oluşturulmaz kapanıyordu. 3.15 ile bağlam yöneticisi yineleme ya da await süresince açık kalır:

python
from contextlib import contextmanager

@contextmanager
def bolum(ad):
    print("başla:", ad)
    yield
    print("bitir:", ad)

@bolum("rapor")
def satirlar():
    yield "satır 1"
    yield "satır 2"

g = satirlar()
print("üretici oluşturuldu")
for s in g:
    print(s)
# çıktı: üretici oluşturuldu
# çıktı: başla: rapor
# çıktı: satır 1
# çıktı: satır 2
# çıktı: bitir: rapor

Python 3.14 ve öncesinde bu örnekte "başla" ve "bitir" satırları yinelemeden önce, üretici oluşturulurken basılırdı. Dekoratör yazımı için Python decorator yazısına bakabilirsiniz.

contextlib araçları

contextlib araçları: suppress(*türler), closing(nesne), nullcontext(değer), redirect_stdout(hedef)

contextlib modülü sık ihtiyaç duyulan bağlam yöneticilerini hazır sunar. Hepsi aynı ilkeye dayanır: girişte bir şey yapar, çıkışta onu geri alır ya da tamamlar.

AraçNe yaparTipik kullanım
suppress(*türler)Belirtilen istisnaları bastırırOlmayabilecek dosyayı silmek
closing(nesne)Blok sonunda nesne.close() çağırırwith desteklemeyen ama close() metodu olan nesneler
nullcontext(değer)Hiçbir şey yapmaz, değeri döndürürİsteğe bağlı bir bağlam yöneticisinin yerini tutmak
redirect_stdout(hedef)sys.stdout akışını geçici olarak yönlendirirprint çıktısını yakalamak
redirect_stderr(hedef)sys.stderr akışını yönlendirirHata çıktısını yakalamak
chdir(yol)Çalışma klasörünü geçici olarak değiştirir (3.11+)Belirli bir klasörde komut çalıştırmak
ExitStack()Değişken sayıda yöneticiyi ve geri çağırmayı yönetirSayısı çalışma anında belli olan dosyalar

suppress, try/except/pass yapısının kısa yazımıdır. Belgeler, hataları tamamen bastıran her mekanizma gibi bunun da yalnızca sessizce devam etmenin doğru olduğu bilinen dar durumlarda kullanılmasını önerir. closing ve nullcontext ise sırasıyla close() çağrısını garanti etmek ve "bazen yönetici var, bazen yok" durumunu tek bir with ile yazmak için kullanılır:

python
import os
from contextlib import suppress, closing, nullcontext

with suppress(FileNotFoundError):
    os.remove("olmayan_dosya.tmp")
print("silme denendi, hata yok")

class Akis:
    def oku(self):
        return "veri"
    def close(self):
        print("akış kapatıldı")

with closing(Akis()) as a:
    print(a.oku())

def isle(dosya_ya_da_yol):
    if isinstance(dosya_ya_da_yol, str):
        cm = open(dosya_ya_da_yol, encoding="utf-8")
    else:
        cm = nullcontext(dosya_ya_da_yol)
    with cm as f:
        return f.readline().strip()

with open("baslik.txt", "w", encoding="utf-8") as f:
    f.write("Başlık satırı\n")
print(isle("baslik.txt"))
with open("baslik.txt", encoding="utf-8") as acik:
    print(isle(acik), acik.closed)
# çıktı: silme denendi, hata yok
# çıktı: veri
# çıktı: akış kapatıldı
# çıktı: Başlık satırı
# çıktı: Başlık satırı False

Son satırda dışarıdan verilen açık dosya isle() içinde kapatılmadı: nullcontext çıkışta hiçbir şey yapmadığı için dosyanın sahibi çağıran kod olarak kaldı. redirect_stdout çıktıyı yakalamak için, chdir ise çalışma klasörünü geçici değiştirmek için kullanılır. Belgelere göre ikisi de genel (global) durumu değiştirdiği için kütüphane kodunda ve çok iş parçacıklı uygulamalarda uygun değildir, ama küçük betiklerde kullanışlıdır:

python
import io
import os
from contextlib import redirect_stdout, chdir
from pathlib import Path

tampon = io.StringIO()
with redirect_stdout(tampon):
    print("bu satır ekrana değil tampona gider")
print("yakalanan:", repr(tampon.getvalue()))

Path("alt").mkdir(exist_ok=True)
once = os.getcwd()
with chdir("alt"):
    print("içeride:", Path.cwd().name)
print("geri döndü mü:", os.getcwd() == once)
# çıktı: yakalanan: 'bu satır ekrana değil tampona gider\n'
# çıktı: içeride: alt
# çıktı: geri döndü mü: True

ExitStack: değişken sayıda kaynak

Açılacak dosyaların sayısı ancak çalışma anında belliyse iç içe with yazamazsınız. ExitStack bu sorunu çözer: enter_context(yönetici) bir bağlam yöneticisine girer ve çıkışını yığına ekler, callback(fonksiyon, *args) çıkışta çalışacak sıradan bir fonksiyon kaydeder. Yığın kapanırken kayıtlar ters sırayla çalışır, bu da iç içe with deyimleriyle aynı davranıştır. Listedeki dosyalardan biri açılamazsa daha önce açılanlar yine kapatılır:

python
from contextlib import ExitStack

adlar = ["ocak.txt", "subat.txt", "mart.txt"]
for i, ad in enumerate(adlar, start=1):
    with open(ad, "w", encoding="utf-8") as f:
        f.write(f"{i * 100}\n")

with ExitStack() as yigin:
    dosyalar = [yigin.enter_context(open(ad, encoding="utf-8")) for ad in adlar]
    yigin.callback(print, "tüm dosyalar kapatılıyor")
    print(sum(int(f.read()) for f in dosyalar))
print([f.closed for f in dosyalar])
# çıktı: 600
# çıktı: tüm dosyalar kapatılıyor
# çıktı: [True, True, True]

pop_all() metodu yığındaki kayıtları yeni bir ExitStack'e aktarır ve hiçbirini çalıştırmaz; belgelerdeki tarif, bir grup dosyayı "ya hepsi ya hiçbiri" mantığıyla açmak için bunu kullanır. ExitStack nesneleri çöp toplayıcı tarafından silindiğinde kayıtlı geri çağırmaları kendiliğinden çalıştırmaz; kapanışı with ya da close() ile tetiklemeniz gerekir. Python 3.15 ile ExitStack ve AsyncExitStack, with deyimiyle tutarlı olacak şekilde __enter__() ve __exit__() metotları keyfi tanımlayıcılarla (descriptor) tanımlanmış nesneleri de kabul etmeye başladı. Bağlam yöneticisi olmayan bir nesne verirseniz 3.11'den beri TypeError alırsınız.

async with

Eşzamansız (asynchronous) kodda aynı fikir async with ile uygulanır; yöneticinin __aenter__() ve __aexit__() metotları await edilir. contextlib.asynccontextmanager, aclosing() ve AsyncExitStack bu tarafın karşılıklarıdır. Ayrıntılar için Python asyncio yazısına bakabilirsiniz.

Sık yapılan hatalar

  • @contextmanager içinde try/finally unutmak: Bloktaki bir istisna yield satırında fırlatılır ve temizlik kodu hiç çalışmaz.
  • Üreticide istisnayı yakalayıp yeniden fırlatmamak: Hata sessizce bastırılır ve program yanlış durumda devam eder.
  • __exit__ metodunda istemeden True döndürmek: Örneğin bir değişkeni döndürürken onun doğru (truthy) olması tüm hataları yutar; açıkça False ya da None döndürün.
  • Tek kullanımlık yöneticiyi saklayıp yeniden kullanmak: Dosyalar ve @contextmanager nesneleri ikinci with'te çalışmaz; yöneticiyi with satırında oluşturun.
  • with bloğundan sonra dosyayı kullanmak: Blok bittiğinde dosya kapanır; f.read() çağrısı ValueError: I/O operation on closed file. verir.
  • suppress ile geniş türleri bastırmak: suppress(Exception) gerçek hataları da gizler; yalnızca beklediğiniz dar türleri yazın.
  • redirect_stdout ve chdir'i kütüphane kodunda kullanmak: İkisi de genel durumu değiştirir ve iş parçacıklarıyla güvenli değildir.

Sık Sorulan Sorular

with open kullanınca close() çağırmak gerekir mi?

Hayır. with bloğu bittiğinde dosya otomatik olarak kapanır; bloğun içinde istisna çıksa bile. Bloktan sonra f.closed değeri True olur.

Context manager ne demek?

with deyimiyle kullanılabilen, yani __enter__() ve __exit__() metotlarını tanımlayan nesnedir. Girişte bir kaynağı hazırlar, çıkışta onu serbest bırakır ya da önceki duruma döndürür. Dosyalar, kilitler ve veritabanı bağlantıları tipik örneklerdir.

Sınıf mı yazmalıyım, @contextmanager mı kullanmalıyım?

Basit "önce şunu yap, sonra geri al" senaryoları için @contextmanager daha kısadır. Yöneticinin durum taşıması, birden fazla kez girilebilmesi (yeniden kullanılabilir olması) ya da başka metotlar sunması gerekiyorsa sınıf yazmak daha uygundur.

with içinde return yazarsam kaynak kapanır mı?

Evet. Blok return, break veya continue ile terk edildiğinde de __exit__() çağrılır; bu durumda üç argüman da None olur ve dönüş değeri yok sayılır.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Python öğrenme rehberi · Python try except · Python dosya okuma ve yazma · Python generator ve yield · Python decorator · Python dunder metotları · Python asyncio