Python’da dataclasses: Sınıf Yazmanın Kısa Yolu

Python'da @dataclass ile __init__, __repr__ ve __eq__ yazmaya son. default_factory, __post_init__, frozen, order, slots ve asdict seçeneklerini çalışan örneklerle inceliyoruz.

Python’da yalnızca veri taşıyan basit bir sınıf yazmak bile şaşırtıcı derecede uzun sürer. Önce __init__ içinde her alanı tek tek atarsınız. Ekrana anlamlı bir çıktı vermesi için __repr__ yazarsınız, iki nesneyi karşılaştırabilmek için de __eq__. Üç alanlı bir sınıf böylece 13 satırı buluyor. dataclasses modülü bu satırların hepsini sizin yerinize yazıyor. Bu yazıda @dataclass dekoratörünü temel kullanımdan slots ve frozen gibi ileri seçeneklere kadar, çalışan örneklerle inceliyoruz.

Neden dataclass?

Önce klasik yöntemle yazılmış bir kitap sınıfına bakalım:

class BookClassic:
    def __init__(self, title, author, pages):
        self.title = title
        self.author = author
        self.pages = pages

    def __repr__(self):
        return f"BookClassic(title={self.title!r}, author={self.author!r}, pages={self.pages!r})"

    def __eq__(self, other):
        if not isinstance(other, BookClassic):
            return NotImplemented
        return (self.title, self.author, self.pages) == (other.title, other.author, other.pages)

Aynı sınıfın dataclass karşılığı yalnızca beş satır:

from dataclasses import dataclass

@dataclass
class Book:
    title: str
    author: str
    pages: int

@dataclass, tip açıklaması (type hint) verilmiş alanları okur ve __init__, __repr__ ile __eq__ metotlarını otomatik olarak üretir. Bir alan eklediğinizde üç metodu ayrı ayrı güncellemeniz gerekmez, böylece birini unutma riskiniz de ortadan kalkar. Tip açıklamaları çalışma anında denetlenmez ama editörünüz ve mypy gibi araçlar bunları kullanır. Bu konuda daha fazla bilgi için Python 3.12 tip sistemi yazımıza göz atabilirsiniz.

Bu yazıdaki örneklerin tamamı tek bir main.py dosyasında. Her başlık dosyadaki bir demo_ fonksiyonuna karşılık geliyor.

Kodun Anatomisi

Proje bilgisi Yazar: Ahmet Aksoy · Tarih: 08.10.2026 Ortam: Python 3.12.3, Ubuntu 24.04 · Gereken en düşük sürüm: Python 3.10

1. Temel kullanım: repr ve eşitlik hazır geliyor

a = Book("Tutunamayanlar", "Oğuz Atay", 724)
b = Book("Tutunamayanlar", "Oğuz Atay", 724)
print(a)
print("a == b       :", a == b)

# Çıktı:
# Book(title='Tutunamayanlar', author='Oğuz Atay', pages=724)
# a == b       : True

Normal bir sınıfta a == b sonucu False olurdu, çünkü Python iki farklı nesnenin bellekteki kimliğini karşılaştırırdı. Dataclass ise alanları sırayla karşılaştırır. Bu davranış özellikle testlerde işinizi kolaylaştırır: beklenen nesneyi oluşturup assert sonuc == beklenen yazmanız yeterlidir.

2. Varsayılan değerler ve değiştirilebilir varsayılan tuzağı

Python’un bilinen tuzaklarından biri, liste gibi değiştirilebilir (mutable) bir nesneyi varsayılan değer olarak vermektir. Böyle bir liste bütün nesneler arasında paylaşılır. Dataclass bu hatayı sınıf tanımlanırken yakalar ve yerine default_factory kullanmanızı ister:

from dataclasses import field

@dataclass
class Shelf:
    name: str
    books: list[Book] = field(default_factory=list)
    capacity: int = 20

s1, s2 = Shelf("Roman"), Shelf("Bilim")
s1.books.append(Book("Saatleri Ayarlama Enstitüsü", "Ahmet Hamdi Tanpınar", 382))
print("s1 kitap sayısı:", len(s1.books))   # 1
print("s2 kitap sayısı:", len(s2.books))   # 0

@dataclass
class BadShelf:
    books: list = []

# Çıktı:
# ValueError: mutable default <class 'list'> for field books is not allowed: use default_factory

default_factory=list, her yeni nesne için list() çağırır ve böylece her rafın kendi listesi olur. Bir kuralı da unutmayın: varsayılan değeri olan alanlar, varsayılanı olmayanlardan sonra gelmelidir. Aksi halde TypeError alırsınız.

3. post_init: Doğrulama ve hesaplanan alanlar

Otomatik üretilen __init__ yalnızca atama yapar. Gelen değeri denetlemek ya da başka alanlardan yeni bir değer hesaplamak istiyorsanız __post_init__ metodunu tanımlarsınız. Bu metot, __init__ işini bitirir bitirmez çağrılır:

@dataclass
class Order:
    product: str
    unit_price: float
    quantity: int = 1
    total: float = field(init=False)

    def __post_init__(self):
        if self.quantity < 1:
            raise ValueError(f"quantity must be >= 1, got {self.quantity}")
        self.total = round(self.unit_price * self.quantity, 2)

print(Order("Kahve", 145.50, 3))
Order("Çay", 40.0, 0)

# Çıktı:
# Order(product='Kahve', unit_price=145.5, quantity=3, total=436.5)
# ValueError: quantity must be >= 1, got 0

field(init=False) ile işaretlenen total, yapıcıda (constructor) parametre olarak yer almaz ama repr çıktısında görünür. Daha kapsamlı doğrulama gerekiyorsa, örneğin dışarıdan gelen JSON verisini denetleyecekseniz, FastAPI yazımızda kullandığımız Pydantic daha uygun bir araçtır. Uygulamanın içinde dolaşan veriler için ise dataclass yeterlidir ve ek bir paket gerektirmez.

4. frozen=True: Değiştirilemeyen nesneler

Koordinat ve para tutarı gibi değerlerin oluşturulduktan sonra değişmemesi gerekir. frozen=True atamayı engeller ve nesneyi hash’lenebilir yapar:

from dataclasses import FrozenInstanceError, replace

@dataclass(frozen=True)
class Point:
    lat: float
    lon: float

istanbul = Point(41.01, 28.98)
istanbul.lat = 0                       # FrozenInstanceError
moved = replace(istanbul, lon=29.10)   # yeni nesne üretir
visited = {istanbul: "İstanbul", Point(39.93, 32.86): "Ankara"}
print(visited[Point(41.01, 28.98)])

# Çıktı:
# dataclasses.FrozenInstanceError: cannot assign to field 'lat'
# replace      : Point(lat=41.01, lon=29.1)
# sözlük anahtarı: İstanbul

replace() orijinal nesneye dokunmaz. Belirttiğiniz alanları değiştirerek yeni bir kopya döndürür. Python 3.15 yazımızda ele aldığımız frozendict ile aynı düşünceye dayanır: değişmeyen veri, hatası daha kolay bulunan kod demektir.

5. order=True: Nesneleri sıralamak

order=True, <, <=, > ve >= operatörlerini üretir. Karşılaştırma, alanlar tanımlandıkları sırayla yapılır. compare=False ile işaretlenen bir alan ise karşılaştırmaya katılmaz:

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int = 0
    label: str = field(default="", compare=False)

versions = [Version(3, 15, 0, "rc"), Version(3, 9), Version(3, 12, 3), Version(3, 10, 14)]
print([f"{v.major}.{v.minor}.{v.patch}" for v in sorted(versions)])

# Çıktı:
# ['3.9.0', '3.10.14', '3.12.3', '3.15.0']

Sürümleri metin olarak sıralasaydınız "3.10" değeri "3.9" değerinden önce gelirdi. Sayısal alanlar sayesinde sıralama doğru çıkıyor.

6. kw_only ve slots: Daha güvenli ve daha hafif nesneler

Python 3.10 ile iki kullanışlı seçenek geldi. kw_only=True parametrelerin isimle verilmesini zorunlu kılar. Böylece User("Ayşe", "ayse@ornek.com", 31) çağrısında sırayı karıştırma ihtimali ortadan kalkar. slots=True ise nesnenin özelliklerini bir sözlükte değil sabit yuvalarda saklar. Bu hem bellek tasarrufu sağlar hem de yazım hatasıyla yeni bir özellik eklenmesini önler:

@dataclass(slots=True, kw_only=True)
class UserSlots:
    name: str
    email: str
    age: int

UserSlots("Ayşe", "ayse@ornek.com", 31)
# TypeError: UserSlots.__init__() takes 1 positional argument but 4 were given

u = UserSlots(name="Ayşe", email="ayse@ornek.com", age=31)
u.nickname = "ayse31"
# AttributeError: 'UserSlots' object has no attribute 'nickname'

Peki ne kadar bellek kazanılıyor? main.py, tracemalloc ile 100.000 kullanıcı nesnesinin kapladığı belleği ölçüyor:

100.000 nesne (normal):   19,806 KB
100.000 nesne (slots) :   16,678 KB  (%16 daha az)

Bu hesapta isim ve e-posta metinleri de yer alıyor. Asıl kazanç nesne başına yaklaşık 32 bayt. Az sayıda nesneyle çalışırken fark hissedilmez, ama milyonlarca kayıt tutan bir programda ciddi bir tasarruf sağlar.

7. asdict: JSON’a tek adımda

asdict() bir dataclass’ı, içindeki dataclass’larla birlikte, sözlüğe dönüştürür. Sonucu doğrudan json.dumps fonksiyonuna verebilirsiniz:

shelf = Shelf("Roman", [Book("İnce Memed", "Yaşar Kemal", 436)], capacity=10)
print(json.dumps(asdict(shelf), ensure_ascii=False, indent=2))

# Çıktı:
# {
#   "name": "Roman",
#   "books": [
#     {
#       "title": "İnce Memed",
#       "author": "Yaşar Kemal",
#       "pages": 436
#     }
#   ],
#   "capacity": 10
# }

Aynı yöntem bir API yanıtı hazırlamak ya da kayıtları dosyaya yazmak için de kullanılabilir. Veritabanına kaydetmek isterseniz astuple() ile alanları sırasıyla alıp SQLite yazımızdaki parametreli sorgulara verebilirsiniz.

Nasıl Çalıştırılır?

Üçüncü taraf hiçbir paket gerekmiyor. slots ve kw_only seçenekleri için Python 3.10 veya daha yeni bir sürüm yeterli:

python3 main.py

Ne Zaman dataclass, Ne Zaman Başka Bir Şey?

İhtiyaç Uygun araç
Uygulama içinde veri taşıyan sınıflar @dataclass
Değişmeyen değer nesneleri @dataclass(frozen=True)
Dışarıdan gelen veriyi ayrıştırıp doğrulamak Pydantic
Yalnızca birkaç alan içeren, demet gibi davranan kayıt typing.NamedTuple
Çok sayıda davranış ve iç durum barındıran nesne Normal sınıf

Ne Öğrendik?

  • @dataclass, __init__, __repr__ ve __eq__ metotlarını tip açıklamalarından otomatik üretir. Üç alanlı sınıfımız 13 satırdan 5 satıra indi.
  • field(default_factory=list), değiştirilebilir varsayılan değer tuzağını önler. Dataclass bu hatayı tanım sırasında yakalar.
  • __post_init__, doğrulama ve hesaplanan alanlar için kullanılır.
  • frozen=True ve replace(), değişmeyen ve hash’lenebilen değer nesneleri sağlar.
  • order=True, nesneleri alan sırasına göre sıralanabilir yapar.
  • slots=True ve kw_only=True, nesneleri daha hafif hale getirir ve yanlış kullanıma karşı korur. Ölçümümüzde bellek kullanımı %16 azaldı.
  • asdict(), iç içe nesneleri bile JSON’a hazır bir sözlüğe dönüştürür.

Kaynaklar ve Sonraki Adımlar

Sonraki adım olarak projelerinizde yalnızca veri taşıyan bir sınıf bulun ve onu dataclass’a dönüştürün. Silinen satır sayısını görünce bu dekoratörü bir daha bırakmak istemeyeceksiniz.

Ahmet Aksoy

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