Python'da CSV dosyası okumak ve yazmak için standart csv modülü kullanılır. Satırları liste olarak okumak için csv.reader(), yazmak için csv.writer(); başlık satırındaki sütun adlarıyla sözlük olarak çalışmak için csv.DictReader ve csv.DictWriter vardır. En önemli kural, dosyayı her zaman newline="" ile açmaktır. Noktalı virgülle ayrılmış Türkçe dosyalar için delimiter=";", başında BOM bulunan UTF-8 dosyalar için encoding="utf-8-sig" kullanırsınız.

Bu yazıda tırnaklama kurallarını, newline="" verilmediğinde ne olduğunu, eksik ve fazla alanların nasıl işlendiğini, sayıların neden metin olarak geldiğini, Sniffer ile ayırıcı tahminini ve lehçe (dialect) kavramını Python 3.15.2 ile çalıştırılmış örneklerle göreceksiniz. CSV biçiminin kendisi, RFC 4180 kuralları ve JSON'a dönüşüm CSV biçimi ve JSON dönüşümü yazısında anlatılıyor; burada yalnızca Python modülüne odaklanıyoruz. Serinin tamamı için Python öğrenme rehberi sayfasına bakabilirsiniz.

Neden kendi split(",") kodunuz yetmez?

Neden kendi split(",") kodunuz yetmez?

Belgeler CSV'nin uzun süre standart bir tanımı olmadan kullanıldığını, bu yüzden farklı uygulamaların ürettiği dosyalarda ince farklar bulunduğunu belirtir. Basit görünen bir satır bile tuzak taşıyabilir: alan içinde virgül varsa alan tırnak içine alınır, alanın içindeki tırnak işareti iki kez yazılır ("") ve bir alan birden fazla satıra yayılabilir. satir.split(",") bunların hiçbirini doğru işlemez. csv modülü bu ayrıntıları sizin yerinize halleder.

python
import csv

satirlar = [
    ["ad", "şehir", "not"],
    ["Ayşe", "İzmir", "Teslim edildi"],
    ["Mehmet", "Ankara", 'Kapıda "acil" yazıyor'],
    ["Zeynep", "Bursa, Nilüfer", "İki satırlık\nnot"],
]
with open("musteriler.csv", "w", newline="", encoding="utf-8") as f:
    yazici = csv.writer(f)
    yazici.writerows(satirlar)

with open("musteriler.csv", newline="", encoding="utf-8") as f:
    okuyucu = csv.reader(f)
    for satir in okuyucu:
        print(okuyucu.line_num, satir)
# çıktı: 1 ['ad', 'şehir', 'not']
# çıktı: 2 ['Ayşe', 'İzmir', 'Teslim edildi']
# çıktı: 3 ['Mehmet', 'Ankara', 'Kapıda "acil" yazıyor']
# çıktı: 5 ['Zeynep', 'Bursa, Nilüfer', 'İki satırlık\nnot']

Dosyanın ham içeriğine bakınca yazıcının ne yaptığı görülür: “Bursa, Nilüfer” alanı virgül içerdiği için tırnağa alındı, “Kapıda "acil" yazıyor” alanındaki tırnaklar iki kez yazıldı ve iki satırlık not tırnak içinde yeni satırıyla birlikte saklandı. Satır sonları da \r\n oldu; bu, excel lehçesinin varsayılan satır sonlandırıcısıdır. Okurken son kaydın line_num değerinin 4 değil 5 olduğuna dikkat edin. Belgeler bu özniteliğin okunan kayıt sayısını değil, kaynaktan okunan satır sayısını verdiğini, kayıtların birden fazla satıra yayılabileceğini belirtir. Hata mesajlarında satır numarası gösterirken bu farkı hesaba katın.

newline="" kuralı

Belgelerdeki dipnot açıktır: dosya newline="" ile açılmazsa tırnaklı alanların içindeki yeni satırlar doğru yorumlanmaz ve yazarken \r\n kullanan platformlarda fazladan bir \r eklenir. Modül satır sonlarını kendisi işlediği için newline="" vermek her zaman güvenlidir. Windows'ta bu kuralı unutmanın sonucu, Excel'de her kaydın arasında boş bir satır görünmesidir. Aşağıdaki örnek parametre verilmeden yazılan dosyanın ham baytlarını gösteriyor; çıktı Windows'ta alındı.

python
import csv

with open("bos_satir.csv", "w", encoding="utf-8") as f:   # newline="" yok
    csv.writer(f).writerows([["a", "b"], ["c", "d"]])
with open("bos_satir.csv", "rb") as f:
    print(f.read())
# çıktı: b'a,b\r\r\nc,d\r\r\n' 

Her satırın sonunda \r\r\n var: yazıcı \r\n yazdı, metin kipindeki dosya da \n karakterini Windows satır sonuna çevirerek bir \r daha ekledi. Linux ve macOS'ta aynı kod farkı göstermeyebilir, ama kodunuzun her platformda doğru çalışması için parametreyi her zaman yazın. Dosya açma kipleri ve newline parametresinin genel davranışı dosya okuma ve yazma yazısında anlatılıyor.

DictReader ve DictWriter ile sütun adlarıyla çalışmak

DictReader her satırı, anahtarları sütun adları olan bir sözlüğe çevirir. fieldnames verilmezse ilk satır başlık kabul edilir ve sonuçlara dahil edilmez. Python 3.8'den beri dönen satırlar düz dict nesneleridir (3.6 ve 3.7'de OrderedDict idi). DictWriter ise sözlükleri satırlara çevirir; DictReader sınıfının aksine fieldnames parametresi zorunludur, çünkü sütun sırasını o belirler. Başlık satırını yazmak için writeheader() çağırırsınız.

python
import csv

with open("urunler.csv", "w", newline="", encoding="utf-8") as f:
    alanlar = ["kod", "ad", "fiyat"]
    yazici = csv.DictWriter(f, fieldnames=alanlar)
    yazici.writeheader()
    yazici.writerow({"kod": "K-01", "ad": "Kalem", "fiyat": 12.5})
    yazici.writerow({"kod": "D-07", "ad": "Defter"})

with open("urunler.csv", newline="", encoding="utf-8") as f:
    print(f.read().splitlines())
# çıktı: ['kod,ad,fiyat', 'K-01,Kalem,12.5', 'D-07,Defter,']

with open("urunler.csv", newline="", encoding="utf-8") as f:
    okuyucu = csv.DictReader(f)
    print(okuyucu.fieldnames)
    toplam = 0.0
    for satir in okuyucu:
        print(satir)
        if satir["fiyat"]:
            toplam += float(satir["fiyat"])
    print("toplam:", toplam)
# çıktı: ['kod', 'ad', 'fiyat']
# çıktı: {'kod': 'K-01', 'ad': 'Kalem', 'fiyat': '12.5'}
# çıktı: {'kod': 'D-07', 'ad': 'Defter', 'fiyat': ''}
# çıktı: toplam: 12.5

İki ayrıntı önemli. Birincisi, ikinci üründe fiyat anahtarı yoktu; DictWriter bu alana restval değerini, yani varsayılan olarak boş metni yazdı. İkincisi, okunan fiyat değeri 12.5 sayısı değil '12.5' metnidir. Belgelere göre okuyucu, QUOTE_NONNUMERIC seçeneği verilmedikçe hiçbir otomatik tip dönüşümü yapmaz; her alan metin olarak gelir. Sayılarla hesap yapmadan önce int() ya da float() ile kendiniz çevirmeniz, boş alanları da ayrıca ele almanız gerekir.

Eksik ve fazla alanlar: restkey, restval, extrasaction

Gerçek dünyadaki dosyalarda her satırın başlıkla aynı sayıda alanı olmayabilir. DictReader bir satırda başlıktan fazla alan bulursa fazlalıkları bir listeye koyar ve restkey anahtarıyla (varsayılan None) saklar; eksik alanları ise restval değeriyle (varsayılan None) doldurur. DictWriter tarafında ise fieldnames içinde olmayan bir anahtar içeren sözlük yazmaya çalışırsanız, varsayılan extrasaction="raise" ayarı nedeniyle ValueError alırsınız. extrasaction="ignore" fazla anahtarları yok sayar.

python
import csv, io

try:
    with open("hata.csv", "w", newline="", encoding="utf-8") as f:
        csv.DictWriter(f, fieldnames=["ad"]).writerow({"ad": "Ali", "yas": 30})
except ValueError as e:
    print(e)
# çıktı: dict contains fields not in fieldnames: 'yas'
tampon = io.StringIO()
csv.DictWriter(tampon, fieldnames=["ad"], extrasaction="ignore").writerow({"ad": "Ali", "yas": 30})
print(repr(tampon.getvalue()))
# çıktı: 'Ali\r\n'
veri = ["ad,soyad", "Ali,Kaya,fazla", "Veli"]
for s in csv.DictReader(veri, restkey="ekstra", restval="-"):
    print(s)
# çıktı: {'ad': 'Ali', 'soyad': 'Kaya', 'ekstra': ['fazla']}
# çıktı: {'ad': 'Veli', 'soyad': '-'}

Bu örnekte iki pratik teknik daha var. io.StringIO, dosya oluşturmadan bellekte CSV üretmek için kullanılabilir; bir web uygulamasında indirme yanıtı hazırlarken işe yarar. DictReader ve reader ise dosya nesnesi yerine metin satırlarından oluşan herhangi bir yinelenebilir nesneyi kabul eder; belgeler de modülün metin ayrıştırmayı doğrudan desteklemediğini, ancak bir liste vererek kolayca yapılabileceğini gösterir.

Türkçe dosyalar: noktalı virgül, ondalık virgül ve BOM

Türkçe bölgesel ayarlarda ondalık ayırıcı virgül olduğu için tablo programlarından dışa aktarılan dosyalarda alan ayırıcı olarak noktalı virgülle sık karşılaşırsınız. Bu tür dosyalarda iki ayrı sorun vardır. İlki ayırıcıdır: delimiter=";" vermezseniz her satır tek bir alan gibi okunur. İkincisi sayılardır: 45,90 metni float() ile çevrilemez, önce virgülü noktaya çevirmeniz gerekir. Üçüncü bir ayrıntı da dosyanın başındaki BOM'dur. codecs belgeleri, Microsoft'un UTF-8 dosyaların başına imza olarak EF BB BF baytlarını yazan bir varyant geliştirdiğini, Python'un buna utf-8-sig dediğini anlatır: bu codec yazarken BOM'u ekler, okurken varsa atlar.

python
import csv

icerik = "Ürün;Fiyat;Adet\r\nÇay;45,90;2\r\nŞeker;32,50;1\r\n"
with open("excel_tr.csv", "w", newline="", encoding="utf-8-sig") as f:
    f.write(icerik)

with open("excel_tr.csv", newline="", encoding="utf-8") as f:
    print(next(csv.reader(f, delimiter=";")))
# çıktı: ['\ufeffÜrün', 'Fiyat', 'Adet']
with open("excel_tr.csv", newline="", encoding="utf-8-sig") as f:
    okuyucu = csv.DictReader(f, delimiter=";")
    for satir in okuyucu:
        fiyat = float(satir["Fiyat"].replace(",", "."))
        print(satir["Ürün"], fiyat * int(satir["Adet"]))
# çıktı: Çay 91.8
# çıktı: Şeker 32.5

with open("cikti_tr.csv", "w", newline="", encoding="utf-8-sig") as f:
    yazici = csv.writer(f, delimiter=";")
    yazici.writerow(["Ürün", "Tutar"])
    yazici.writerow(["Çay", "91,80"])
with open("cikti_tr.csv", "rb") as f:
    print(f.read())
# çıktı: b'\xef\xbb\xbf\xc3\x9cr\xc3\xbcn;Tutar\r\n\xc3\x87ay;91,80\r\n' 

İlk okuma, BOM'lu dosyayı düz utf-8 ile açmanın sonucunu gösteriyor: ilk sütun adı '\ufeffÜrün' oldu. Ekranda görünmeyen bu karakter yüzünden satir["Ürün"] araması KeyError verir ve hata çok kafa karıştırıcıdır, çünkü yazdırdığınızda anahtar doğru görünür. utf-8-sig ile açınca sorun ortadan kalktı. Yazarken utf-8-sig kullanmak, dosyayı açacak programın kodlamayı tanımasına yardımcı olabilir; ancak aynı belge UTF-8'de BOM kullanımının genel olarak önerilmediğini de not eder. Dosya başka bir Python programı ya da bir sunucu tarafından okunacaksa BOM'suz utf-8 daha temizdir. Kodlama sorunlarının genel çözümü için Unicode ve Türkçe karakter yazısına bakın.

Tırnaklama seçenekleri: QUOTE_* sabitleri

Tırnaklama seçenekleri: QUOTE_* sabitleri: QUOTE_MINIMAL (varsayılan), QUOTE_ALL, QUOTE_NONNUMERIC, QUOTE_NONE

Yazıcının hangi alanları tırnağa alacağını quoting parametresi belirler. Varsayılan QUOTE_MINIMAL yalnızca ayırıcı, tırnak karakteri ya da satır sonu içeren alanları tırnaklar. QUOTE_NONNUMERIC sayı olmayan tüm alanları tırnaklar ve okuyucuya, tırnaksız alanları float olarak çevirmesini söyler; modülün otomatik tip dönüşümü yaptığı tek durum budur. Python 3.12'de eklenen QUOTE_STRINGS ve QUOTE_NOTNULL ise None değerini tırnaksız boş alan olarak yazar ve okurken tırnaksız boş alanı None olarak geri verir; böylece “boş metin” ile “değer yok” ayrımı korunabilir.

python
import csv, io

t = io.StringIO()
w = csv.writer(t, quoting=csv.QUOTE_NONNUMERIC)
w.writerow(["Kalem", 12.5, 3, None])
print(repr(t.getvalue()))
# çıktı: '"Kalem",12.5,3,""\r\n'
for s in csv.reader(io.StringIO(t.getvalue()), quoting=csv.QUOTE_NONNUMERIC):
    print(s)
# çıktı: ['Kalem', 12.5, 3.0, '']
t2 = io.StringIO()
csv.writer(t2, quoting=csv.QUOTE_STRINGS).writerow(["Kalem", 12.5, None])
print(repr(t2.getvalue()))
# çıktı: '"Kalem",12.5,\r\n'
print(next(csv.reader(io.StringIO(t2.getvalue()), quoting=csv.QUOTE_STRINGS)))
# çıktı: ['Kalem', 12.5, None]

Çıktıda görülen iki ayrıntıya dikkat edin. QUOTE_NONNUMERIC ile tam sayı 3, okunurken 3.0 oldu; dönüşüm her zaman float yönündedir. Aynı kipte None boş tırnaklı metin olarak yazıldı ve geri okunduğunda '' oldu; yani None bilgisi kayboldu. Belgeler genel olarak da yazıcının None değerini boş metin olarak yazdığını, bunun geri döndürülemez bir dönüşüm olduğunu, ama SQL NULL değerlerini CSV'ye dökmeyi kolaylaştırdığını belirtir. QUOTE_STRINGS ise None değerini koruyarak geri verdi.

SabitYazarkenOkurken
QUOTE_MINIMAL (varsayılan)Yalnızca özel karakter içeren alanlarTırnakları kaldırır, her şey metin
QUOTE_ALLTüm alanlarTırnakları kaldırır, her şey metin
QUOTE_NONNUMERICSayı olmayan alanlarTırnaksız alanlar float olur
QUOTE_NONEHiçbiri; özel karakter escapechar ile kaçırılırTırnaklara özel işlem yapmaz
QUOTE_STRINGS (3.12)Metin alanları; None boş ve tırnaksızTırnaksız boş alan None, diğer tırnaksızlar float
QUOTE_NOTNULL (3.12)None olmayan tüm alanlarTırnaksız boş alan None

Lehçeler (dialect) ve Sniffer

Lehçe, bir CSV biçimini tanımlayan ayar grubudur: delimiter, quotechar, doublequote, escapechar, lineterminator, quoting, skipinitialspace ve strict. Hazır üç lehçe vardır: excel (virgül, \r\n), excel-tab (sekme) ve unix (\n satır sonu, tüm alanlar tırnaklı). Aynı ayarları birçok yerde kullanacaksanız register_dialect() ile bir ad altında kaydedebilirsiniz. Belgelerdeki bir not: okuyucu lineterminator ayarını yok sayar; satır sonu olarak her zaman \r ya da \n karakterlerini tanır.

Dosyanın biçimini bilmiyorsanız csv.Sniffer bir örnek metinden lehçeyi tahmin eder ve has_header() ile ilk satırın başlık olup olmadığını kestirir. Belgeler has_header() yönteminin kaba bir sezgisel yöntem olduğunu, hem yanlış pozitif hem yanlış negatif sonuç verebileceğini açıkça söyler. Python 3.15'te sniff() metodunun ayırıcı tespiti hızlandırıldı; davranışında bir değişiklik yok.

python
import csv

ornek = "ad;yas;sehir\nAli;30;İzmir\nAyşe;28;Ankara\n"
lehce = csv.Sniffer().sniff(ornek)
print(repr(lehce.delimiter), csv.Sniffer().has_header(ornek))
# çıktı: ';' True
for s in csv.reader(ornek.splitlines(), lehce):
    print(s)
# çıktı: ['ad', 'yas', 'sehir']
# çıktı: ['Ali', '30', 'İzmir']
# çıktı: ['Ayşe', '28', 'Ankara']
print(csv.list_dialects())
# çıktı: ['excel', 'excel-tab', 'unix']
csv.register_dialect("tr", delimiter=";", quoting=csv.QUOTE_MINIMAL)
print(csv.get_dialect("tr").delimiter)
# çıktı: ;

Gerçek bir dosyada Sniffer kullanırken belgelerdeki kalıbı izleyin: dosyadan bir parça okuyun (f.read(1024)), lehçeyi tahmin edin, ardından f.seek(0) ile başa dönüp okuyucuyu bu lehçeyle oluşturun. Tahminin tutmadığı durumlarda yedek olarak sabit bir lehçe kullanmak iyi bir alışkanlıktır.

Bozuk girdiler ve strict kipi

Varsayılan olarak okuyucu hoşgörülüdür ve bozuk satırları elinden geldiğince okur. strict=True verdiğinizde ise hatalı CSV girdisinde csv.Error fırlatır. Belgelerdeki önerilen hata raporlama kalıbı, istisnayı yakalayıp dosya adını ve reader.line_num değerini mesaja eklemektir. Böylece büyük bir dosyada sorunlu satırı hemen bulursunuz.

python
import csv

try:
    list(csv.reader(['a,"b"c', 'x'], strict=True))
except csv.Error as e:
    print(type(e).__name__ + ":", e)
# çıktı: Error: ',' expected after '"'
print(list(csv.reader(['a,"b"c'])))
# çıktı: [['a', 'bc']]

Hoşgörülü kipte aynı satır sessizce ['a', 'bc'] olarak okundu. Veri doğruluğunun önemli olduğu içe aktarma işlerinde strict=True kullanıp hatalı satırları ayrıca raporlamak, yanlış verinin sessizce veritabanına girmesinden iyidir. CSV'den okuduğunuz verileri kalıcı olarak saklamak istiyorsanız sqlite3 yazısındaki executemany() örneği, satırları tek seferde tabloya eklemenin yolunu gösteriyor.

Sık yapılan hatalar

Sık yapılan hatalar: Dosyayı newline="" olmadan açmak. Windows'ta boş satırlar, çok satırlı..., Okunan değerlerin sayı olduğunu varsaymak. Tüm alanlar metindir; int() ve...
  • Dosyayı newline="" olmadan açmak. Windows'ta boş satırlar, çok satırlı alanlarda bozulma görülür.
  • Okunan değerlerin sayı olduğunu varsaymak. Tüm alanlar metindir; int() ve float() ile çevirin, boş alanları ayrıca ele alın.
  • Noktalı virgüllü dosyayı varsayılan virgül ayırıcıyla okumak ve her satırı tek alan olarak almak.
  • BOM'lu dosyada ilk sütun adının \ufeff ile başladığını fark etmemek. utf-8-sig ile açın.
  • DictWriter ile fazladan anahtar içeren sözlük yazıp ValueError almak. extrasaction="ignore" ya da doğru fieldnames kullanın.
  • Satırları str.split(",") ile bölmek. Tırnaklı alanlar ve gömülü virgüller bozulur.

Sık Sorulan Sorular

CSV dosyasını sözlük listesine nasıl çeviririm?

with open(yol, newline="", encoding="utf-8") as f: kayitlar = list(csv.DictReader(f)) yazmanız yeterlidir. Her öğe başlık satırındaki adları anahtar olarak kullanan bir sözlüktür. Bu listeyi json modülü ile doğrudan JSON'a da yazabilirsiniz.

Excel'de Türkçe karakterler bozuk görünüyor, ne yapmalıyım?

Dosyayı encoding="utf-8-sig" ile yazmak, başına BOM ekleyerek açan programın UTF-8 olduğunu anlamasına yardımcı olur. Ayırıcı olarak da kullanıcının bölgesel ayarına uygun karakteri (Türkçe ayarlarda sık görülen noktalı virgül gibi) seçin.

Büyük bir CSV dosyasını belleği doldurmadan nasıl işlerim?

Okuyucu nesneleri yineleyicidir ve satırları birer birer okur. list() ile hepsini belleğe almak yerine for satir in okuyucu: döngüsüyle işleyin; bellek kullanımı dosya boyutundan bağımsız kalır.

Başlık satırını nasıl atlarım?

reader kullanıyorsanız döngüden önce next(okuyucu) çağırın; bu ilk satırı okur ve atlar. DictReader ise başlığı zaten alan adı olarak kullanır ve sonuçlara dahil etmez.

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 · CSV biçimi ve JSON dönüşümü · json modülü · sqlite3 veritabanı · dosya okuma ve yazma · Unicode ve Türkçe karakterler · pathlib