dataclasses modülündeki @dataclass dekoratörü, tip notuyla (annotation) yazılmış sınıf değişkenlerini alan (field) olarak kabul eder ve bu alanlardan __init__, __repr__ ve __eq__ gibi özel metotları otomatik üretir. ad: str ve fiyat: float yazdığınız bir sınıf, tek satır __init__ yazmadan Urun('Defter', 35.5) şeklinde kurulabilir, okunaklı biçimde yazdırılabilir ve alan alan karşılaştırılabilir. Python eğitimi de birkaç adlandırılmış veriyi bir arada tutmak için bu yolu deyimsel (idiomatic) yöntem olarak önerir.

Bu yazıda @dataclass'ın parametrelerini (frozen, order, slots, kw_only), field() fonksiyonunu, değiştirilebilir varsayılan değer tuzağını, __post_init__ ve InitVar'ı, kalıtımda alan sırasını, asdict(), replace() ve match desteğini Python 3.15.2 ile çalıştırılmış örneklerle anlatıyoruz. Elle yazılan sınıflarla karşılaştırma yapabilmek için önce class ve nesne ile özel metotlar yazılarına, genel yol haritası için Python öğrenme rehberine bakabilirsiniz.

İlk dataclass: neler üretilir?

@dataclass sınıfı inceler, tip notu olan her sınıf değişkenini alan sayar ve alanları tanım sırasıyla kullanır. Belgelere göre iki istisna dışında tip notunun içeriğine bakmaz; yani fiyat: float yazmanız, alana tam sayı verilmesini engellemez. Dekoratör yeni bir sınıf oluşturmaz, aynı sınıfa metot ekleyip onu döndürür (slots=True bunun istisnasıdır).

python
from dataclasses import dataclass

@dataclass
class Urun:
    ad: str
    fiyat: float
    stok: int = 0

    def toplam_deger(self) -> float:
        return self.fiyat * self.stok

u = Urun('Defter', 35.5, 10)
print(u)
print(u == Urun('Defter', 35.5, 10), u is Urun('Defter', 35.5, 10))
print(u.toplam_deger())
print(Urun('Kalem', 12))
print(Urun.__hash__)
# çıktı: Urun(ad='Defter', fiyat=35.5, stok=10)
# çıktı: True False
# çıktı: 355.0
# çıktı: Urun(ad='Kalem', fiyat=12, stok=0)
# çıktı: None

Çıktılar üretilen metotları gösteriyor: __repr__ sınıf adını ve her alanın repr'ini yazdı; __eq__ iki ayrı nesneyi alanları aynı olduğu için eşit buldu; Kalem örneğinde fiyat tam sayı kaldı, çünkü tip notu denetlenmiyor. Son satırdaki None önemlidir: varsayılan ayarlarla (eq=True, frozen=False) __hash__ None yapılır ve nesneler hash'lenemez olur. Değişebilen bir nesnenin hash'lenmemesi doğru davranıştır; aşağıda frozen=True ile bunu nasıl değiştireceğinizi göreceksiniz. Tip notlarının kendisi için type hints yazısına bakabilirsiniz.

Python 3.13 ile üretilen __eq__'nun çalışma biçimi değişti. Önceki sürümler alanları demete toplayıp demetleri karşılaştırıyordu; 3.13'ten beri her alan tek tek karşılaştırılıyor (self.a == other.a and ...). Belgelere göre bu daha hızlıdır, ancak float('nan') gibi kimlikle eşit ama değerle eşit olmayan alanlarda sonucu değiştirebilir. Karşılaştırılan iki nesnenin tipi de birebir aynı olmalıdır.

@dataclass parametreleri

@dataclass parametreleri: init, repr, True, order

Dekoratörü parantezsiz yazmak, bütün parametreleri varsayılan değerleriyle vermekle aynıdır. En sık kullanılanlar şunlardır:

ParametreVarsayılanEtkisi
initTrue__init__ üretir; sınıfta zaten __init__ varsa yok sayılır
reprTrue__repr__ üretir
eqTrue__eq__ üretir; alanları sırayla karşılaştırır
orderFalse__lt__, __le__, __gt__, __ge__ üretir; eq=False ile birlikte ValueError
frozenFalseAlanlara atamayı FrozenInstanceError ile engeller
unsafe_hashFalseGüvenli olmasa da __hash__ üretmeye zorlar
match_argsTruematch deyimi için __match_args__ üretir
kw_onlyFalseBütün alanları yalnızca anahtar kelimeyle verilebilir yapar
slotsFalse__slots__ üretir ve yeni bir sınıf döndürür
weakref_slotFalse__weakref__ slot ekler; slots=True gerektirir

Hash kuralları biraz incelik ister. eq ve frozen ikisi de doğruysa __hash__ üretilir. eq doğru, frozen yanlışsa __hash__ None yapılır. eq yanlışsa taban sınıfın hash'i (sonuçta kimliğe dayalı hash) kullanılır. Belgeler unsafe_hash=True'yu yalnızca mantıksal olarak değişmeyen ama teknik olarak değiştirilebilen özel durumlar için, dikkatle kullanmayı önerir.

Değiştirilebilir varsayılan değer ve default_factory

Varsayılan değerler sınıf özniteliği olarak saklandığı için, liste gibi değiştirilebilir bir varsayılan bütün örnekler arasında paylaşılırdı. Bu, sıradan sınıflarda de bilinen bir tuzaktır. @dataclass hash'lenemeyen bir varsayılan değer gördüğünde bunu değiştirilebilirlik işareti sayar ve sınıfı oluşturmayı reddeder. Doğru yol field(default_factory=list) yazmaktır; fabrika fonksiyonu her nesne için ayrıca çağrılır:

python
from dataclasses import dataclass, field

try:
    @dataclass
    class Sepet:
        urunler: list = []
except ValueError as e:
    print(e)

@dataclass
class Sepet:
    sahip: str
    urunler: list[str] = field(default_factory=list)

a = Sepet('Ayşe')
b = Sepet('Can')
a.urunler.append('çay')
print(a, b)
print(a.urunler is not b.urunler)
# çıktı: mutable default <class 'list'> for field urunler is not allowed: use default_factory
# çıktı: Sepet(sahip='Ayşe', urunler=['çay']) Sepet(sahip='Can', urunler=[])
# çıktı: True

Python 3.11 öncesinde yalnızca list, dict ve set tipleri yasaklanıyordu; 3.11'den beri hash'lenemeyen her nesne varsayılan değer olarak reddediliyor. Bu kısmi bir korumadır: hash'lenebilen ama değiştirilebilen bir nesne yine de paylaşılabilir. default_factory sıfır argümanlı herhangi bir çağrılabilir olabilir; örneğin field(default_factory=lambda: {'rol': 'okur'}).

field(): alan bazında ayar

field(): alan bazında ayar

field() fonksiyonu tek bir alanın davranışını değiştirir. default ya da default_factory (ikisi birlikte verilemez), init=False ile __init__'ten çıkarma, repr=False ile yazdırmadan gizleme, compare=False ile karşılaştırmaya katmama, kw_only=True ile anahtar kelime zorunluluğu ve metadata ile üçüncü taraf araçlara bilgi taşıma en sık kullanılan parametrelerdir. Python 3.14 ile doc parametresi eklendi. Aşağıda sıralamaya yalnızca öncelik alanı katılıyor:

python
from dataclasses import dataclass, field

@dataclass(order=True)
class Gorev:
    oncelik: int
    baslik: str = field(compare=False)
    notlar: str = field(default='', repr=False, compare=False)

isler = [Gorev(3, 'Yedek al'), Gorev(1, 'Sunucuyu güncelle'), Gorev(2, 'Log temizle', 'acil değil')]
for g in sorted(isler):
    print(g)
print(Gorev(1, 'a') == Gorev(1, 'b'))
try:
    Gorev(1, 'a') < 5
except TypeError as e:
    print(e)
# çıktı: Gorev(oncelik=1, baslik='Sunucuyu güncelle')
# çıktı: Gorev(oncelik=2, baslik='Log temizle')
# çıktı: Gorev(oncelik=3, baslik='Yedek al')
# çıktı: True
# çıktı: '<' not supported between instances of 'Gorev' and 'int'

order=True ile üretilen karşılaştırmalar nesneyi alanlarından oluşan bir demet gibi sıralar; compare=False alanlar bu demete girmez. Bu yüzden başlıkları farklı iki görev eşit sayıldı. Farklı tiple sıralama denendiğinde de olağan TypeError oluştu. Sınıf zaten bir sıralama metodu tanımlıyorsa order=True TypeError verir.

repr=False, parola ya da erişim anahtarı gibi değerlerin günlük kayıtlarına ve hata çıktılarına sızmasını önlemenin basit bir yoludur. metadata ise dataclass mekanizmasının hiç kullanmadığı, yalnızca sizin ya da üçüncü taraf araçların okuması için alana iliştirilen bir eşlemedir; salt okunur bir sarmalayıcıyla saklanır. Alanların tanımlarına fields() ile ulaşırsınız; her biri name, type, default, repr, compare, metadata gibi öznitelikleri olan bir Field nesnesidir. Belgeler Field nesnelerini kendiniz oluşturmamanızı, yalnızca fields()'ten okumanızı söyler:

python
from dataclasses import dataclass, field, fields

@dataclass
class Sunucu:
    ad: str
    ip: str = field(metadata={'aciklama': 'IPv4 adresi'})
    sifre: str = field(default='', repr=False)

s = Sunucu('web1', '203.0.113.10', 'gizli')
print(s)
for f in fields(Sunucu):
    print(f.name, f.repr, dict(f.metadata))
# çıktı: Sunucu(ad='web1', ip='203.0.113.10')
# çıktı: ad True {}
# çıktı: ip True {'aciklama': 'IPv4 adresi'}
# çıktı: sifre False {}

Alan sırası kuralı ve kalıtım

Üretilen __init__, alanları parametre olarak sırayla dizer. Python fonksiyonlarında varsayılanı olmayan bir parametre, varsayılanı olan bir parametreden sonra gelemez; bu yüzden dataclass'ta da varsayılansız bir alan, varsayılanlı alandan sonra yazılamaz. Kural kalıtımla birleşen alanlar için de geçerlidir:

python
from dataclasses import dataclass

try:
    @dataclass
    class Hatali:
        x: int = 0
        y: int
except TypeError as e:
    print(e)

@dataclass
class Taban:
    ad: str
    aktif: bool = True

try:
    @dataclass
    class Alt(Taban):
        rol: str            # varsayılansız alan, varsayılanlı alandan sonra
except TypeError as e:
    print(e)
# çıktı: non-default argument 'y' follows default argument 'x'
# çıktı: non-default argument 'rol' follows default argument 'aktif'

Kalıtımda dekoratör taban sınıfları ters MRO sırasıyla (yani object'ten başlayarak) dolaşır, bulduğu her dataclass'ın alanlarını sıralı bir eşlemeye ekler, en son kendi alanlarını ekler. Alt sınıf aynı adlı bir alanı yeniden tanımlarsa alan eski yerinde kalır ama tipi ve varsayılanı alt sınıftaki olur. Belgelerdeki örnek bunu gösteriyor:

python
from dataclasses import dataclass
import inspect

@dataclass
class Taban:
    x: float = 15.0
    y: int = 0

@dataclass
class C(Taban):
    z: int = 10
    x: int = 15

print(inspect.signature(C))
print(C())
# çıktı: (x: int = 15, y: int = 0, z: int = 10) -> None
# çıktı: C(x=15, y=0, z=10)

Varsayılan sırası sorununu çözmenin temiz bir yolu anahtar kelime zorunlu alanlardır. kw_only=True ya da _: KW_ONLY işaretinden sonraki alanlar, __init__'te * işaretinin sağına taşınır; bu parametrelerde varsayılan sırası kuralı aranmaz. Konumsal ve anahtar kelime parametreleri için *args ve **kwargs yazısına bakabilirsiniz.

__post_init__, init=False ve InitVar

Üretilen __init__ alanları atadıktan sonra, sınıfta tanımlıysa __post_init__'i çağırır. Doğrulama yapmak ya da başka alanlardan hesaplanan bir alanı doldurmak için doğru yer burasıdır. Hesaplanan alan field(init=False) ile __init__ parametrelerinden çıkarılır. InitVar ile işaretlenen bir alan ise gerçek alan değildir: __init__'e parametre olarak eklenir, __post_init__'e iletilir, ama nesnede saklanmaz ve fields() sonucunda görünmez:

python
from dataclasses import dataclass, field, InitVar, fields

@dataclass
class Fatura:
    tutar: float
    kdv_orani: float = 0.20
    toplam: float = field(init=False)
    indirim: InitVar[float] = 0.0

    def __post_init__(self, indirim):
        if self.tutar < 0:
            raise ValueError('tutar negatif olamaz')
        self.toplam = round((self.tutar - indirim) * (1 + self.kdv_orani), 2)

print(Fatura(100))
print(Fatura(100, indirim=10))
print([f.name for f in fields(Fatura)])
try:
    Fatura(-5)
except ValueError as e:
    print(e)
# çıktı: Fatura(tutar=100, kdv_orani=0.2, toplam=120.0)
# çıktı: Fatura(tutar=100, kdv_orani=0.2, toplam=108.0)
# çıktı: ['tutar', 'kdv_orani', 'toplam']
# çıktı: tutar negatif olamaz

Belgelerin önemli bir notu: üretilen __init__ taban sınıfın __init__'ini çağırmaz. Taban sınıf dataclass değilse ve başlatılması gerekiyorsa bunu __post_init__ içinde super().__init__(...) ile yapmak yaygın yöntemdir. Taban sınıf da dataclass ise gerek yoktur, çünkü alt sınıfın __init__'i bütün alanları zaten atar. Sınıfta kendi __init__'inizi yazarsanız __post_init__ otomatik çağrılmaz.

frozen=True: değişmeyen nesneler

Belgeler Python'da gerçekten değiştirilemez nesne oluşturmanın mümkün olmadığını, frozen=True'nun bunu taklit ettiğini söyler. Dekoratör sınıfa, çağrıldığında FrozenInstanceError fırlatan __setattr__ ve __delattr__ ekler. Bu istisna AttributeError'ın alt sınıfıdır. Dondurulmuş ve eq=True olan sınıflar hash'lenebilir; sözlük anahtarı ya da küme elemanı olarak kullanılabilir. Değiştirilmiş bir kopya için dataclasses.replace() ya da Python 3.13 ile gelen genel copy.replace() kullanılır:

python
from dataclasses import dataclass, FrozenInstanceError, replace
import copy

@dataclass(frozen=True)
class Koordinat:
    enlem: float
    boylam: float

ist = Koordinat(41.01, 28.97)
try:
    ist.enlem = 0
except FrozenInstanceError as e:
    print(type(e).__name__, '-', e)
print(issubclass(FrozenInstanceError, AttributeError))
print(hash(ist) == hash(Koordinat(41.01, 28.97)))
print({ist: 'İstanbul'}[Koordinat(41.01, 28.97)])
print(replace(ist, boylam=29.0))
print(copy.replace(ist, enlem=40.0))
# çıktı: FrozenInstanceError - cannot assign to field 'enlem'
# çıktı: True
# çıktı: True
# çıktı: İstanbul
# çıktı: Koordinat(enlem=41.01, boylam=29.0)
# çıktı: Koordinat(enlem=40.0, boylam=28.97)

replace() yeni nesneyi __init__ üzerinden oluşturur, böylece __post_init__ doğrulamaları da yeniden çalışır. init=False alanlar ise kaynaktan kopyalanmaz, __post_init__'te yeniden hesaplanır; bu tür alanları replace()'e argüman olarak vermek ValueError verir. frozen=True küçük bir performans bedeliyle gelir: __init__ alanları basit atamayla değil object.__setattr__() ile kurmak zorundadır.

slots, KW_ONLY, asdict ve JSON

python
from dataclasses import dataclass, KW_ONLY, asdict, astuple
import json

@dataclass(slots=True)
class Kullanici:
    ad: str
    _: KW_ONLY
    eposta: str
    yonetici: bool = False

k = Kullanici('Deniz', eposta='deniz@example.com')
print(k)
try:
    Kullanici('Deniz', 'deniz@example.com')
except TypeError as e:
    print(e)
print(asdict(k))
print(astuple(k))
print(json.dumps(asdict(k), ensure_ascii=False))
print(hasattr(k, '__dict__'), Kullanici.__slots__)
# çıktı: Kullanici(ad='Deniz', eposta='deniz@example.com', yonetici=False)
# çıktı: Kullanici.__init__() takes 2 positional arguments but 3 were given
# çıktı: {'ad': 'Deniz', 'eposta': 'deniz@example.com', 'yonetici': False}
# çıktı: ('Deniz', 'deniz@example.com', False)
# çıktı: {"ad": "Deniz", "eposta": "deniz@example.com", "yonetici": false}
# çıktı: False ('ad', 'eposta', 'yonetici')

slots=True sınıfa __slots__ ekler ve örneklerde __dict__ bulunmaz; bellek ve hız avantajlarıyla birlikte dinamik öznitelik eklenemez (ayrıntılar property ve __slots__ yazısında). Belgeler, slots=True kullanılırken taban sınıfın __init_subclass__'ına parametre geçmenin TypeError verdiğini de uyarır. asdict() nesneyi sözlüğe, astuple() demete çevirir; iç içe dataclass, liste, demet, sözlük ve (3.15 ile) frozendict değerlerine özyinelemeli iner, diğer nesneleri copy.deepcopy() ile kopyalar. Sözlüğe çevirdikten sonra JSON'a yazmak tek satırdır (json modülü yazısı).

match deyimiyle kullanım

match_args=True varsayılanı sayesinde dataclass'lar __match_args__ demetini alır; bu demet, sınıf desenlerinde konumsal argümanların hangi alanlara karşılık geldiğini belirler. Anahtar kelime zorunlu alanlar bu demete girmez:

python
from dataclasses import dataclass

@dataclass
class Nokta:
    x: int
    y: int

def yer(p):
    match p:
        case Nokta(0, 0):
            return 'orijin'
        case Nokta(x, 0):
            return f'x ekseni, x={x}'
        case Nokta():
            return 'başka bir nokta'

print(Nokta.__match_args__)
print(yer(Nokta(0, 0)), '|', yer(Nokta(5, 0)), '|', yer(Nokta(1, 2)))
# çıktı: ('x', 'y')
# çıktı: orijin | x ekseni, x=5 | başka bir nokta

Desen eşleştirmenin ayrıntıları için match-case yazısına bakabilirsiniz. Python 3.15 ile üretilen __init__ metotlarının tip notları artık iç tip adlarını içermiyor; bunu yukarıdaki inspect.signature() örneğinde olduğu gibi imzayı incelerken görebilirsiniz. Ayrıca MISSING ve KW_ONLY 3.15'te yeni sentinel tipinin nesneleri oldu ve metadata verilmediğinde boş bir frozendict kullanılıyor (Python 3.15 yenilikleri).

dataclass mı, namedtuple mı, sıradan class mı?

dataclass mı, namedtuple mı, sıradan class mı? Alanlı, değişebilen kayıt, Değişmeyen, hash'lenebilen kayıt, Demet gibi indekslenen kayıt, Karmaşık davranış, özel kurulum
İhtiyaçUygun seçimNeden
Alanlı, değişebilen kayıt@dataclass__init__, __repr__, __eq__ hazır gelir
Değişmeyen, hash'lenebilen kayıt@dataclass(frozen=True)Atama engellenir, sözlük anahtarı olabilir
Demet gibi indekslenen kayıtcollections.namedtupletuple alt sınıfıdır, açılabilir (unpacking)
Karmaşık davranış, özel kurulumSıradan classHer şey sizin kontrolünüzde
Sabit seçenekler kümesienum.EnumÜyeler tekil ve adlandırılmış sabitlerdir

namedtuple için collections yazısına, sabit seçenekler için Enum yazısına bakabilirsiniz. Dataclass'lar normal Python sınıfı oldukları için metot, property ve kalıtım gibi bütün sınıf özelliklerini kullanmaya devam edebilirsiniz.

Sık yapılan hatalar

  • Tip notunu unutmak: ad = 'x' şeklinde, notsuz yazılan sınıf değişkeni alan sayılmaz ve __init__'e girmez.
  • Tip notunun denetlendiğini sanmak: @dataclass çalışma anında tip kontrolü yapmaz; gerekiyorsa __post_init__ içinde doğrulayın.
  • Liste varsayılanı: x: list = [] ValueError verir; field(default_factory=list) kullanın.
  • Hash beklemek: Varsayılan dataclass hash'lenemez; küme ya da sözlük anahtarı gerekiyorsa frozen=True kullanın.
  • ClassVar'ı alan sanmak: typing.ClassVar ile işaretlenen değişkenler alan değildir ve fields() sonucunda görünmez.
  • __slots__ ile alan adlarını okumak: Belgeler alan listesini __slots__'tan değil fields()'ten almanızı söyler.

Sık Sorulan Sorular

dataclass tip kontrolü yapar mı?

Hayır. Tip notları yalnızca alanları tanımlamak için kullanılır; yanlış tipte değer verildiğinde hata oluşmaz. Statik denetim için tip denetleyicileri, çalışma anı denetimi için __post_init__ kullanılır.

dataclass nesnesini sözlüğe nasıl çeviririm?

dataclasses.asdict(nesne) iç içe yapılar dahil derin bir sözlük döndürür. Yalnızca üst düzey bir kopya istiyorsanız belgelerdeki öneri {f.name: getattr(nesne, f.name) for f in fields(nesne)} ifadesidir.

Bir nesnenin dataclass olup olmadığını nasıl anlarım?

dataclasses.is_dataclass(x) hem sınıf hem örnek için True döner. Yalnızca örnekleri ayırt etmek için ek olarak not isinstance(x, type) denetimi yapın.

dataclass'a kendi metotlarımı ekleyebilir miyim?

Evet. Dataclass sıradan bir sınıftır; metot, property, classmethod ekleyebilir, kendi __repr__'ınızı yazabilirsiniz. Kendi yazdığınız metot varsa ilgili parametre (örneğin repr=True) yok sayılır ve sizinki korunur.

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 · Class ve nesne · Özel metotlar · property ve __slots__ · Type hints · json modülü