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=Truevereplace(), değişmeyen ve hash’lenebilen değer nesneleri sağlar.order=True, nesneleri alan sırasına göre sıralanabilir yapar.slots=Truevekw_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
- dataclasses — Python belgeleri
- PEP 557 (Data Classes)
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.



