Neden önemli?
Anadolu Market'in gece çalışan işlerini düşün: her mağaza kasası günün satışlarını bir CSV dosyası olarak gelen/2026-10-27/ klasörüne bırakıyor, ödeme sistemi ikili (binary) bir dekont arşivi gönderiyor, muhasebe sabah 08.00'de rapor.json dosyasını okuyor. Bu işte üç şey ters gidebilir: yanlış modda açılan dosya Türkçe karakterleri bozar, bir klasördeki 500 dosyadan biri bozuk olduğu için tüm iş durur, ya da program rapor yazarken çöker ve muhasebe yarım bir JSON okur.
Temel düzeydeki Dosya işlemleri dersinde open, with, okuma-yazma modlarını ve pathlib'in ilk adımlarını gördün. Bu ders o temelin üstüne kurumsal düzeyi koyar: bayt düzeyinde çalışma, dosya içinde gezinme, hata türlerini ayırma, standart akışlar, klasörleri toplu işleme, arşivler ve hiçbir zaman yarım kalmayan yazma.
Metin modu ve ikili mod
Diskte her şey bayttır (byte). Metin modu ("r", "w") baytları senin için bir kodlamayla (encoding) çözer ve str verir; satır sonlarını da işletim sistemine göre çevirir. İkili mod ("rb", "wb") hiçbir çeviri yapmaz, ham bytes verir. Resim, PDF, zip, şifreli dosya ya da özet (hash) hesabı için her zaman ikili mod kullanırsın.
import tempfile, os
with tempfile.TemporaryDirectory() as d:
yol = os.path.join(d, "sehir.txt")
with open(yol, "w", encoding="utf-8") as f:
yazilan = f.write("İzmir\nÇorum\n")
print("yazılan karakter:", yazilan)
with open(yol, "rb") as f:
ham = f.read()
print(type(ham).__name__, len(ham), "bayt")
print(ham)
print(ham.decode("utf-8").splitlines())
12 karakter yazdın ama dosya 14 bayt: UTF-8'de İ ve Ç ikişer bayt tutar. Metin modunda write yazılan karakter sayısını, ikili modda bayt sayısını döndürür.
| Mod | Anlamı | Dosya yoksa | Dosya varsa |
|---|---|---|---|
r / rb | Oku | FileNotFoundError | Baştan okur |
w / wb | Yaz | Oluşturur | İçeriği SİLER |
a / ab | Sona ekle | Oluşturur | Sona yazar |
x / xb | Yalnız yeni dosya | Oluşturur | FileExistsError |
r+ / r+b | Oku ve yaz | FileNotFoundError | Silmeden, imlecin olduğu yere yazar |
encoding vermezsen Python işletim sisteminin varsayılanını kullanır; Windows'ta bu hâlâ cp1254 olabilir. Kendi makinende çalışan betik sunucuda Türkçe karakterleri bozar. Excel'in ürettiği CSV'lerin başındaki BOM işaretini atmak için encoding="utf-8-sig" kullan.bytes ve bytearray: tampon olarak baytlar
bytes değişmez (immutable) bir bayt dizisidir; bytearray ise değiştirilebilir (mutable) kardeşidir. Bir ağ paketini, sabit genişlikli bir banka kaydını ya da parça parça gelen veriyi biriktirirken bytearray bir tampon (buffer) gibi kullanılır: her eklemede yeni nesne üretmez.
imza = b"PK" # zip dosyalarının ilk 4 baytı
print(imza[0], imza[:2], len(imza)) # indeks tek bayt -> int verir
tampon = bytearray()
for parca in [b"AB", b"-", b"2026"]:
tampon.extend(parca) # yerinde büyür
tampon[0] = ord("X") # yerinde değiştir
print(tampon, bytes(tampon))
print("2026".encode("ascii") in tampon)
print(bytes([72, 105]).decode()) # sayılardan bayt
Dikkat: imza[0] bir tam sayı (80) verir, imza[:2] ise bytes. str ile bytes asla karışmaz: b"abc" == "abc" False'tur, b"abc" + "d" TypeError verir. Geçiş her zaman encode (str → bytes) ve decode (bytes → str) ile yapılır.
İmleç: read, readline, seek, tell
Açık bir dosyanın bir imleci (file pointer) vardır. Her okuma imleci ileri taşır. tell() imlecin konumunu, seek(konum, nereden) imleci taşımayı sağlar: nereden 0 = baştan, 1 = bulunduğun yerden, 2 = sondan. İkili modda bunlar bayt cinsindendir; metin modunda seek yalnızca tell'den aldığın değerlerle güvenlidir.
import io f = io.BytesIO(b"ID;TUTAR\n101;250\n102;975\n") # bellek içi ikili dosya print(f.readline()) # ilk satır (başlık) print(f.tell()) # 9 bayt okundu print(f.read(3)) # sonraki 3 bayt f.seek(0) # başa dön print(len(f.readlines()), "satır") f.seek(-4, 2) # sondan 4 bayt geri print(f.read())
io.BytesIO ve io.StringIO diskte dosya açmadan aynı arayüzü verir; testlerde ve küçük denemelerde çok işine yarar. readlines() tüm dosyayı listeye yükler; büyük dosyada bunun yerine for satir in f kullan.
Büyük dosyayı parça parça okumak
5 GB'lık bir işlem arşivinin özetini (hash) hesaplamak ya da satırlarını saymak için dosyayı belleğe almazsın; sabit boyutlu parçalar (chunk) hâlinde okursun. Walrus operatörü (:=) bu döngüyü çok temiz yazar.
import hashlib, io
dosya = io.BytesIO(b"islem;" * 10_000)
ozet = hashlib.sha256()
parca_sayisi = 0
while parca := dosya.read(8192): # boş bytes gelince döngü biter
ozet.update(parca)
parca_sayisi += 1
print(parca_sayisi, "parça")
print(ozet.hexdigest()[:16])
print(ozet.hexdigest() == hashlib.sha256(b"islem;" * 10_000).hexdigest())
Bellek kullanımı dosya boyutundan bağımsızdır: hep en fazla 8 KB. Parça sınırına denk gelen bir satır sonu ya da çok baytlı bir harf ikiye bölünebilir; bu yüzden satır mantığı gerekiyorsa son parçayı bir sonrakine taşırsın ya da doğrudan for satir in f kullanırsın (o da içeride tamponlu okur).
Hata türleri: OSError ailesi ve errno
Dosya ve işletim sistemiyle ilgili tüm hatalar OSError'dan türer. Her birinin bir errno kodu (işletim sisteminin hata numarası), bir strerror açıklaması ve çoğunlukla bir filename alanı vardır. Python, errno koduna bakarak doğru alt sınıfı kendisi seçer.
| Alt sınıf | errno | Ne zaman? |
|---|---|---|
FileNotFoundError | ENOENT | Dosya ya da klasör yok |
FileExistsError | EEXIST | "x" modu, mkdir var olan klasörde |
PermissionError | EACCES, EPERM | Okuma/yazma izni yok, dosya başka programda kilitli (Windows) |
IsADirectoryError | EISDIR | Klasörü dosya gibi açmaya çalıştın |
NotADirectoryError | ENOTDIR | Dosyayı klasör gibi kullandın |
import errno, tempfile
try:
open("/yok/boyle/bir/dosya.csv", encoding="utf-8")
except FileNotFoundError as e:
print(type(e).__name__, e.errno == errno.ENOENT, e.filename)
with tempfile.TemporaryDirectory() as d:
try:
open(d, encoding="utf-8")
except IsADirectoryError as e:
print("Klasör açılamaz:", e.strerror)
except PermissionError as e: # bazı sistemler klasör için bunu verir
print("İzin hatası:", e.strerror)
hata = OSError(errno.EACCES, "Erişim reddedildi")
print(type(hata).__name__, isinstance(hata, OSError))
Son satır önemli: OSError(errno.EACCES, ...) yazdığında Python sana doğrudan PermissionError verir. Bu yüzden ya alt sınıfları yakalarsın (okunur), ya da OSError yakalayıp e.errno ile ayırırsın (daha ayrıntılı). Özelden genele sırala: önce FileNotFoundError, en sonda OSError.
Standart akışlar: stdin, stdout, stderr
Her programın üç hazır akışı vardır: sys.stdin (girdi), sys.stdout (normal çıktı) ve sys.stderr (hata ve uyarı çıktısı). Bunlar da dosya nesnesidir; write, readline, for satir in sys.stdin ile kullanılır. Kural basit: sonuç stdout'a, gürültü stderr'e. Böylece python rapor.py > rapor.csv diye yönlendiren biri dosyasına uyarı mesajı karışmadan yalnız veriyi alır.
import sys, io, contextlib
kayitlar = ["Ankara;250", "Bursa;abc", "İzmir;975"]
cikti, hatalar = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(cikti), contextlib.redirect_stderr(hatalar):
for k in kayitlar:
sehir, tutar = k.split(";")
if tutar.isdigit():
print(f"{sehir},{tutar}")
else:
print(f"UYARI: geçersiz tutar -> {k}", file=sys.stderr)
print("stdout:", cikti.getvalue().splitlines())
print("stderr:", hatalar.getvalue().strip())
redirect_stdout/redirect_stderr akışı geçici olarak başka bir dosya nesnesine çevirir; test yazarken programın ne yazdırdığını yakalamanın standart yolu budur. Gerçek bir komut satırı aracında ise for satir in sys.stdin: ile boru hattından (pipe) gelen veriyi satır satır okursun: cat siparis.csv | python temizle.py.
pathlib ileri: klasörü sorgulamak
from pathlib import Path
import tempfile
with tempfile.TemporaryDirectory() as d:
kok = Path(d)
(kok / "2026" / "10").mkdir(parents=True, exist_ok=True) # ara klasörler de
for ad in ["2026/10/ankara.csv", "2026/10/izmir.csv", "2026/notlar.txt", "ozet.csv"]:
(kok / ad).write_text("tarih,tutar\n", encoding="utf-8")
print(sorted(p.name for p in kok.iterdir())) # yalnız bir seviye
print(sorted(p.name for p in kok.glob("*.csv"))) # bu klasörde
print(sorted(p.relative_to(kok).as_posix() for p in kok.rglob("*.csv"))) # tüm alt klasörler
p = kok / "2026/10/ankara.csv"
print(p.stem, p.suffix, p.with_suffix(".json").name, p.parent.name)
print(p.stat().st_size, "bayt", p.exists(), p.is_file())
glob("*.csv") yalnızca o klasöre, rglob("*.csv") tüm alt ağaca bakar (glob("**/*.csv") ile aynı). iterdir() ve glob sırası işletim sistemine göre değişir; raporda tekrarlanabilir sonuç için her zaman sırala. stem son uzantı hariç ad, suffix son uzantı, suffixes tüm uzantılardır (.tar.gz için iki tane).
shutil, tempfile, zipfile ve tarfile
shutil dosya düzeyinde üst işlemleri yapar: copy (içerik + izinler), copy2 (tarihler de), copytree (klasör ağacı), move (taşı/yeniden adlandır), rmtree (klasörü içiyle sil, geri dönüşü yok). tempfile güvenli geçici dosya ve klasör verir; TemporaryDirectory blok bitince her şeyi temizler.
import shutil, tempfile, zipfile, tarfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
kok = Path(d)
kaynak = kok / "rapor"
kaynak.mkdir()
(kaynak / "ciro.csv").write_text("sehir,tutar\nAnkara,250\n", encoding="utf-8")
(kaynak / "not.txt").write_text("gece işi tamam", encoding="utf-8")
shutil.copy(kaynak / "ciro.csv", kaynak / "ciro_yedek.csv")
zip_yolu = kok / "rapor.zip"
with zipfile.ZipFile(zip_yolu, "w", compression=zipfile.ZIP_DEFLATED) as z:
for p in sorted(kaynak.rglob("*")):
z.write(p, arcname=p.relative_to(kaynak).as_posix())
with zipfile.ZipFile(zip_yolu) as z:
print(z.namelist())
print(z.read("ciro.csv").decode("utf-8").splitlines()[1])
with tarfile.open(kok / "rapor.tar.gz", "w:gz") as t:
t.add(kaynak, arcname="rapor")
with tarfile.open(kok / "rapor.tar.gz") as t:
print(sorted(m.name for m in t.getmembers() if m.isfile()))
shutil.rmtree(kaynak)
print(kaynak.exists())
../../etc/passwd gibi adlar hedef klasörün dışına yazabilir (path traversal). tarfile için Python 3.12'de t.extractall(hedef, filter="data") kullan; zip'te her adın Path(hedef, ad).resolve() sonucunun hedefin içinde kaldığını kontrol et.Atomik yazma ve dosya kilidi
Program open("rapor.json", "w") ile dosyayı açtığı anda eski içerik silinir. Yazma sürerken elektrik kesilirse ya da program çökerse geride yarım bir dosya kalır. Çözüm: önce aynı klasörde geçici bir dosyaya yaz, kapat, sonra os.replace ile asıl adın üzerine taşı. os.replace aynı dosya sisteminde atomiktir: okuyan kişi ya eski dosyanın tamamını ya yeni dosyanın tamamını görür, arası yoktur.
import os, json, tempfile
def atomik_json_yaz(yol, veri):
klasor = os.path.dirname(os.path.abspath(yol))
fd, gecici = tempfile.mkstemp(dir=klasor, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(veri, f, ensure_ascii=False)
f.flush()
os.fsync(f.fileno()) # diske gerçekten yazıldığından emin ol
os.replace(gecici, yol) # tek hamlede yer değiştir
except BaseException:
os.unlink(gecici) # hata olursa çöp bırakma
raise
with tempfile.TemporaryDirectory() as d:
hedef = os.path.join(d, "rapor.json")
atomik_json_yaz(hedef, {"sehir": "Ankara", "ciro": 1250})
atomik_json_yaz(hedef, {"sehir": "İzmir", "ciro": 975})
print(open(hedef, encoding="utf-8").read(), os.listdir(d))
Geçici dosyanın hedefle aynı klasörde olması şarttır; /tmp başka bir disk bölümündeyse os.replace atomik olamaz ya da hata verir.
İki süreç aynı dosyaya aynı anda yazıyorsa kilit (lock) gerekir. En taşınabilir basit yöntem bir kilit dosyasıdır: open("is.lock", "x") dosya zaten varsa FileExistsError verir, yani kilidi yalnızca bir süreç alabilir; iş bitince os.remove ile bırakırsın. İşletim sistemi düzeyinde kilit için Linux/macOS'ta fcntl.flock, Windows'ta msvcrt.locking, ikisini birden saran üçüncü parti filelock paketi vardır.
try/finally içinde bırak, kilit dosyasına süreç numarası (PID) ve zaman yaz ki eski kilitleri tanıyabilesin.Klasörü toplu işlemek
Gerçek işlerin çoğu şu kalıptır: bir klasördeki dosyaları bul, her birini oku, bozuk olanı atla ama kaydını tut, sonucu tek bir rapora atomik yaz.
import csv, tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as d:
kok = Path(d)
(kok / "ankara.csv").write_text("sehir,tutar\nAnkara,250\nAnkara,100\n", encoding="utf-8")
(kok / "izmir.csv").write_text("sehir,tutar\nİzmir,975\nİzmir,yok\n", encoding="utf-8")
toplam, hatali = {}, []
for yol in sorted(kok.glob("*.csv")):
with yol.open(encoding="utf-8", newline="") as f:
for no, satir in enumerate(csv.DictReader(f), start=2):
try:
tutar = float(satir["tutar"])
except ValueError:
hatali.append(f"{yol.name}:{no}")
continue
toplam[satir["sehir"]] = toplam.get(satir["sehir"], 0) + tutar
print(toplam)
print("hatalı satırlar:", hatali)
arsiv/ klasörüne shutil.move ile taşımak, hatalıyı hatali/ klasörüne ayırmak ve sonucu atomik yazmak. Hatalı satırın dosya adı ve satır numarasıyla loglanması, sabah gelen "rakam tutmuyor" e-postasını beş dakikada çözmeni sağlar. Aynı dosyanın iki kez işlenmemesi için işlenmiş dosyaların özetini (sha256) bir listede tutmak da yaygın bir pratiktir.Arabellek, flush ve fsync
write veriyi önce Python'un arabelleğine (buffer), sonra işletim sisteminin önbelleğine (cache) yazar; diske ne zaman gideceğine işletim sistemi karar verir. f.flush() Python arabelleğini boşaltır, os.fsync(f.fileno()) işletim sisteminden diske yazmasını ister. Para ve sipariş gibi kaybı kabul edilemeyen veride ikisini birlikte kullanırsın; her satırda yapmak ise çok yavaştır, toplu yaparsın.
os.scandir ve büyük klasörler
Yüz binlerce dosyalı klasörde os.scandir, dosya türü ve boyut bilgisini tek sistem çağrısıyla getirdiği için os.listdir + os.stat'tan belirgin hızlıdır; pathlib'in iterdir'i de içeride benzer çalışır.
mmap ve readinto
mmap modülü dosyayı belleğe eşler; çok büyük ikili dosyada rastgele konumlara seek etmeden dilimle erişirsin. f.readinto(bytearray(n)) ise var olan bir tampona okur, her okumada yeni nesne üretmez; yüksek hacimli ikili protokollerde kullanılır.
Kodlama hataları
Kaynağını bilmediğin metin dosyasında errors="replace" bozuk baytları yerine koyma karakteriyle değiştirir, errors="strict" (varsayılan) hata verir. Veri kaybını görmezden gelmek yerine önce hatayı yakalayıp hangi dosyada olduğunu loglamak daha güvenlidir.