Sebagian besar class pada program nyata adalah class data: Produk, Pelanggan, Konfigurasi — objeknya hidup untuk menyimpan sekumpulan nilai. Jika ditulis manual, class seperti itu menuntut tiga method boilerplate yang polanya selalu sama: __init__ menyalin parameter ke atribut, __repr__ merangkai teks debug, dan __eq__ membandingkan isi. Module dataclasses (tersedia sejak Python 3.7) memindahkan ketiganya ke satu decorator @dataclass, sehingga kita cukup mendeklarasikan field beserta tipenya.

Decorator @dataclass dan Method Otomatis #

Saat interpreter Python mengeksekusi @dataclass, decorator tersebut memindai annotasi tipe pada class body, lalu menghasilkan __init__, __repr__, dan __eq__ secara otomatis berdasarkan daftar field. Sintaks decorator @ ini sudah kita pakai pada Chapter classmethod dan staticmethod.

python
>>> from dataclasses import dataclass
>>> @dataclass
... class Produk:
...     nama: str
...     harga: int
... 
>>> Produk("Kopi Gayo", 45000)
Produk(nama='Kopi Gayo', harga=45000)
>>> Produk("Kopi Gayo", 45000) == Produk("Kopi Gayo", 45000)
True

Tiga baris deklarasi nama: str dan harga: int menghasilkan class yang lengkap:

  1. __init__ menerima nama dan harga sebagai parameter positional dan menyimpannya sebagai atribut instance.
  2. __repr__ mencetak Produk(nama='Kopi Gayo', harga=45000) — bentuk yang persis seperti ekspresi pembuatan objeknya, sehingga mudah direkonstruksi saat debugging.
  3. __eq__ membandingkan semua field, bukan identitas objek — dua objek berbeda dengan isi sama dilaporkan True.

Perbandingan dengan Class Manual #

Untuk merasakan penghematan kode, mari kita bandingkan class manual dengan hasil @dataclass secara berdampingan. Kedua class di bawah ini berperilaku identik:

python
class ProdukManual:
    def __init__(self, nama, harga, stok):
        self.nama = nama
        self.harga = harga
        self.stok = stok

    def __repr__(self):
        return (f"ProdukManual(nama={self.nama!r}, "
                f"harga={self.harga!r}, stok={self.stok!r})")

    def __eq__(self, other):
        if not isinstance(other, ProdukManual):
            return NotImplemented
        return (self.nama, self.harga, self.stok) == (other.nama, other.harga, other.stok)


from dataclasses import dataclass


@dataclass
class Produk:
    nama: str
    harga: int
    stok: int = 0


a = Produk("Kopi Gayo", 45000, 10)
b = Produk("Kopi Gayo", 45000, 10)

print(a)
print(a == b)
print(a == Produk("Kopi Toraja", 38000, 5))

m = ProdukManual("Kopi Gayo", 45000, 10)
n = ProdukManual("Kopi Gayo", 45000, 10)

print(m)
print(m == n)

Output yang Dihasilkan:

text
Produk(nama='Kopi Gayo', harga=45000, stok=10)
True
False
ProdukManual(nama='Kopi Gayo', harga=45000, stok=10)
True

Mari kita telaah bagian dataclass pada kode di atas:

  1. Baris 20 (@dataclass): decorator membaca tiga field bernama nama, harga, dan stok dari class body, lalu menghasilkan tiga method otomatis tanpa kita menulis satu baris pun.
  2. Baris 24 (stok: int = 0): field dengan nilai default; Produk("Kopi", 45000) sah karena stok otomatis bernilai 0. Field tanpa default wajib ditulis lebih dulu.
  3. Baris 27 (a = Produk(...)): konstruktor yang dihasilkan menerima ketiga field secara positional maupun keyword — Produk(nama="Kopi Gayo", harga=45000) juga sah; b pada baris berikutnya dibuat dengan isi identik.
  4. Baris 32 (a == Produk(...)): perbandingan menghasilkan False karena field nama dan harga berbeda — sama persis dengan perilaku __eq__ manual pada class di bagian atas.

Empat belas baris class manual runtuh menjadi lima baris deklarasi, dan setiap penambahan field hanya membutuhkan satu baris.

Nilai Default dan field(default_factory=…) #

Field mutable seperti list dan dict tidak boleh dijadikan default secara langsung — Python menolaknya saat decorator dieksekusi. Aturan ini sengaja diberlakukan karena satu objek default akan dibagikan oleh semua instance, persis jebakan mutable default pada Chapter Default Parameters.

python
from dataclasses import dataclass, field

# --- jebakan: default mutable langsung ---
try:
    @dataclass
    class KeranjangSalah:
        pelanggan: str
        items: list = []
except ValueError as e:
    print("ValueError:", e)


# --- solusi: default_factory ---
@dataclass
class Keranjang:
    pelanggan: str
    items: list = field(default_factory=list)


keranjang_budi = Keranjang("Budi")
keranjang_siti = Keranjang("Siti")

keranjang_budi.items.append("kopi")

print(keranjang_budi)
print(keranjang_siti)

Output yang Dihasilkan:

text
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
Keranjang(pelanggan='Budi', items=['kopi'])
Keranjang(pelanggan='Siti', items=[])

Argumen default_factory=list meminta interpreter memanggil list() yang baru setiap kali objek Keranjang dibuat, sehingga keranjang Budi dan Siti benar-benar terpisah — item milik Budi tidak pernah bocor ke keranjang Siti.

Data Immutable dengan frozen=True #

Konfigurasi dan nilai seperti koordinat sering tidak boleh berubah setelah dibuat. Parameter frozen=True menghasilkan class yang menolak penugasan atribut, selaras dengan materi Chapter Mutable dan Immutable Objects.

python
from dataclasses import dataclass


@dataclass(frozen=True)
class Konfigurasi:
    host: str
    port: int


config = Konfigurasi("db.example.com", 5432)
print(config)

try:
    config.port = 3306
except Exception as e:
    print(type(e).__name__ + ":", e)

# frozen dataclass otomatis hashable -> bisa jadi anggota set
db1 = Konfigurasi("db.example.com", 5432)
db2 = Konfigurasi("db.example.com", 5432)
print(len({db1, db2}))

Output yang Dihasilkan:

text
Konfigurasi(host='db.example.com', port=5432)
FrozenInstanceError: cannot assign to field 'port'
1

Penugasan config.port = 3306 memicu FrozenInstanceError — dataclass versi beku memblokir mutasi sejak awal. Sebagai bonus, class beku otomatis hashable, sehingga db1 dan db2 yang setara terhitung sebagai satu anggota set.

slots=True dan order=True #

Dua parameter lain yang sering dipakai:

python
from dataclasses import dataclass


@dataclass(slots=True)
class Titik:
    x: int
    y: int


p = Titik(3, 4)
print(p)

try:
    p.z = 5
except AttributeError as e:
    print("AttributeError:", e)


@dataclass(order=True)
class Versi:
    nomor: int


versi_lama = Versi(2)
versi_baru = Versi(3)

print(versi_lama < versi_baru)
print(sorted([Versi(3), Versi(1), Versi(2)]))

Output yang Dihasilkan:

text
Titik(x=3, y=4)
AttributeError: 'Titik' object has no attribute 'z'
True
[Versi(nomor=1), Versi(nomor=2), Versi(nomor=3)]

Parameter slots=True (Python 3.10+) menghasilkan class hemat memori yang menolak atribut baru — mekanisme lengkapnya dibahas pada Chapter slots. Parameter order=True menghasilkan __lt__, __le__, __gt__, dan __ge__ yang membandingkan field secara berurutan, sehingga sorted() langsung bekerja.