Menulis kelas Python yang hanya berfungsi menyimpan data sering kali berakhir dengan boilerplate yang berulang-ulang: metode __init__ yang panjang, __repr__ agar objek mudah di-debug, dan __eq__ supaya dua objek bisa dibandingkan. Semua itu bisa ditulis manual, tapi melelahkan dan rawan salah. Untungnya, Python punya solusi bawaan sejak versi 3.7: dataclasses. Dengan satu decorator @dataclass — yang pada dasarnya hanyalah sebuah decorator Python biasa — kamu bisa membuat kelas data yang rapi, mudah dibaca, dan otomatis dilengkapi metode-metode penting. Di tutorial ini kita akan membahas dataclass secara menyeluruh, dari dasar hingga fitur canggih seperti field(), frozen, slots, dan __post_init__, lengkap dengan contoh kode yang bisa langsung kamu jalankan.

Apa Itu Dataclass?

Dataclass adalah cara untuk mendeklarasikan kelas yang sebagian besar isinya adalah data. Kamu cukup menuliskan atribut beserta type hint-nya, lalu Python akan men-generate metode-metode umum secara otomatis: __init__, __repr__, __eq__, dan (jika diminta) __lt__, __le__, __gt__, __ge__, serta __hash__. Dataclass tersedia di modul dataclasses dalam pustaka standar, jadi kamu tidak perlu menginstal apa pun.

Sebelum & Sesudah: Hapus Boilerplate

Mari kita lihat perbedaannya secara langsung. Berikut ini kelas data sederhana yang ditulis manual seperti cara tradisional:

python
class ProductManual:
    def __init__(self, name: str, price: float, quantity: int = 0):
        self.name = name
        self.price = price
        self.quantity = quantity

    def __repr__(self):
        return (
            f"ProductManual(name={self.name!r}, "
            f"price={self.price!r}, quantity={self.quantity!r})"
        )

    def __eq__(self, other):
        if not isinstance(other, ProductManual):
            return NotImplemented
        return (
            self.name, self.price, self.quantity
        ) == (other.name, other.price, other.quantity)


p1 = ProductManual("Laptop", 15_000_000, 3)
p2 = ProductManual("Laptop", 15_000_000, 3)
print(p1)          # repr manual
print(p1 == p2)    # True

Bandingkan dengan versi dataclass yang setara:

python
from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float
    quantity: int = 0


p1 = Product("Laptop", 15_000_000, 3)
p2 = Product("Laptop", 15_000_000, 3)
print(p1)          # Product(name='Laptop', price=15000000, quantity=3)
print(p1 == p2)    # True

Perhatikan bahwa dataclass membaca urutan field untuk membuat __init__. Field tanpa nilai default (seperti name dan price) harus ditulis sebelum field yang punya default (seperti quantity), persis seperti aturan parameter fungsi biasa.

Parameter @dataclass: init, repr, eq, order, dan frozen

Decorator @dataclass menerima beberapa argumen untuk mengontrol metode apa saja yang dibuat. Nilai default-nya adalah init=True, repr=True, eq=True, order=False, dan frozen=False.

python
from dataclasses import dataclass


@dataclass(order=True)
class Book:
    title: str
    pages: int


b1 = Book("Python Dasar", 300)
b2 = Book("Python Lanjutan", 500)
print(b1 < b2)   # True -> dibandingkan berdasarkan urutan field (title dulu, lalu pages)

Ketika order=True, Python otomatis membuat metode perbandingan (<, <=, >, >=). Catatan penting: order=True hanya bisa dipakai jika eq=True.

python
from dataclasses import dataclass


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


cfg = Config("localhost")
print(cfg.port)   # 5432
# cfg.port = 3306  # Akan melempar FrozenInstanceError

Dengan frozen=True, objek menjadi immutable (tidak bisa diubah setelah dibuat) dan secara otomatis dapat dipakai sebagai key di dict atau elemen di set, karena __hash__ ikut dibuat.

Nilai Default dan Fungsi field()

Jangan pernah memakai objek mutable (seperti list, dict, atau set) langsung sebagai nilai default. Default tersebut dibuat sekali saat kelas didefinisikan, sehingga semua instance akan berbagi list yang sama dan saling menimpa data satu sama lain.

python
from dataclasses import dataclass, field


@dataclass
class ShoppingCart:
    items: list = field(default_factory=list)  # benar: list baru per instance
    total: float = 0.0


cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append("Mouse")
print(cart1.items)   # ['Mouse']
print(cart2.items)   # []  <- tidak saling berbagi list!

Fungsi field() punya beberapa opsi berguna selain default dan default_factory, yaitu init, repr, dan compare. Contohnya, kita bisa menyembunyikan field sensitif dari repr:

python
from dataclasses import dataclass, field


@dataclass
class User:
    username: str
    password: str = field(repr=False)      # tidak tampil di repr
    roles: list = field(default_factory=list, compare=False)


u = User("budi", "rahasia123")
print(u)   # User(username='budi')  <- password aman

Validasi dan Perhitungan dengan __post_init__

Dataclass menyediakan hook __post_init__ yang dipanggil otomatis setelah __init__ selesai. Ini tempat yang tepat untuk validasi input atau menghitung field turunan yang tidak boleh diisi lewat konstruktor.

python
from dataclasses import dataclass, field


@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # tidak jadi parameter __init__

    def __post_init__(self):
        self.area = self.width * self.height


r = Rectangle(4, 5)
print(r.area)   # 20.0
python
from dataclasses import dataclass


@dataclass
class Temperature:
    celsius: float

    def __post_init__(self):
        if self.celsius < -273.15:
            raise ValueError("Suhu tidak mungkin di bawah nol mutlak")


Temperature(25)      # OK
# Temperature(-300)  # ValueError

Pewarisan (Inheritance) pada Dataclass

Dataclass bisa diwariskan seperti kelas biasa. Field dari kelas induk akan otomatis digabungkan ke dalam __init__ kelas anak.

python
from dataclasses import dataclass


@dataclass
class Animal:
    name: str
    age: int = 0


@dataclass
class Dog(Animal):
    breed: str = "campuran"


d = Dog("Bolt", 3, "Golden Retriever")
print(d)   # Dog(name='Bolt', age=3, breed='Golden Retriever')

Aturan urutan field tetap berlaku saat pewarisan: jika kelas induk punya field dengan nilai default, maka semua field baru di kelas anak juga wajib diberi nilai default, supaya tidak ada parameter non-default yang muncul setelah parameter default.

Fitur Modern: slots=True dan kw_only

Sejak Python 3.10, dataclass mendukung slots=True untuk menghemat memori (terutama saat membuat jutaan instance), dan kw_only=True untuk memaksa semua field diisi lewat keyword argument.

python
from dataclasses import dataclass


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


@dataclass(kw_only=True)
class UserProfile:
    name: str
    email: str
    age: int = 0


u = UserProfile(name="Budi", email="budi@example.com")
print(u)

Konversi ke dict dan tuple: asdict() & astuple()

Modul dataclasses juga menyediakan asdict() dan astuple() untuk mengubah instance menjadi dict atau tuple. Ini sangat berguna saat ingin menyimpan data ke JSON atau menggabungkannya dengan alat analisis data.

python
from dataclasses import dataclass, asdict, astuple


@dataclass
class Point:
    x: int
    y: int


p = Point(3, 4)
print(asdict(p))    # {'x': 3, 'y': 4}
print(astuple(p))   # (3, 4)

Kombinasi yang umum adalah mengubah daftar dataclass menjadi DataFrame untuk analisis lebih lanjut. Jika kamu belum familier, kamu bisa mempelajarinya di panduan Pandas untuk Analisis Data:

python
import pandas as pd
from dataclasses import dataclass, asdict


@dataclass
class Sale:
    product: str
    qty: int
    price: float


sales = [Sale("Mouse", 2, 150_000), Sale("Keyboard", 1, 400_000)]
df = pd.DataFrame([asdict(s) for s in sales])
print(df)

Dataclass vs NamedTuple vs Pydantic

Selain dataclass, Python punya namedtuple dari modul collections untuk hal serupa, tetapi namedtuple bersifat immutable dan lebih terbatas (tidak punya __post_init__ atau field() yang fleksibel). Sementara itu, untuk kebutuhan validasi data yang jauh lebih ketat — misalnya memvalidasi input dari API atau konfigurasi — pustaka seperti Pydantic adalah pilihan yang lebih tepat.

Aturan praktisnya: pakai dataclass untuk struktur data internal aplikasi yang ringan dan cepat, lalu beralih ke Pydantic ketika kamu butuh parsing, validasi, dan serialisasi data dari luar (misalnya request HTTP atau file JSON). Keduanya sering dipakai berdampingan di proyek nyata.

Kapan Menggunakan Dataclass?

Dataclass paling pas untuk: model data sederhana, objek konfigurasi, hasil query database, DTO (Data Transfer Object), dan struktur data internal. Sebaliknya, jangan paksa dataclass untuk kelas yang punya banyak perilaku (metode bisnis kompleks) atau yang membutuhkan validasi eksternal yang ketat.

Dataclass adalah salah satu fitur Python yang paling banyak dipakai di proyek produksi karena menghilangkan boilerplate sekaligus tetap eksplisit dan mudah dibaca. Dengan memahami field(), default_factory, frozen, slots, dan __post_init__, kamu sudah siap memanfaatkannya secara maksimal dalam kode kamu sendiri. Selamat mencoba!