1. Pengenalan Dataclasses
dataclasses adalah modul Python yang diperkenalkan di Python 3.7 (PEP 557) yang menyediakan decorator dan fungsi untuk membuat class yang terutama digunakan untuk menyimpan data. Dengan @dataclass, Python secara otomatis menghasilkan method seperti __init__, __repr__, __eq__, dan lainnya berdasarkan field yang dideklarasikan.
Tanpa dataclass, Anda harus menulis banyak boilerplate code untuk method __init__, __repr__, dan __eq__ secara manual. Dataclass menghilangkan kerumitan ini dan menghasilkan kode yang lebih bersih, lebih singkat, dan lebih mudah dipelihara.
Mengapa Dataclasses?
| Fitur | Penjelasan |
|---|---|
| Kurang Boilerplate | Tidak perlu tulis __init__, __repr__, __eq__ secara manual |
| Type Hints | Didesain untuk digunakan dengan type annotations |
| Immutability | Opsional: buat dataclass frozen (tidak bisa diubah) |
| Field Options | Kontrol default value, factory, comparison, hashing |
| Inheritance | Bisa diwarisi dan dikombinasikan |
| Slots | Optimasi memori dengan __slots__ |
Dataclass vs Regular Class
# === Regular Class (manual, banyak boilerplate) ===
class PenggunaManual:
def __init__(self, nama: str, umur: int, email: str):
self.nama = nama
self.umur = umur
self.email = email
def __repr__(self):
return f"PenggunaManual(nama='{self.nama}', umur={self.umur}, email='{self.email}')"
def __eq__(self, other):
if not isinstance(other, PenggunaManual):
return NotImplemented
return (self.nama == other.nama and
self.umur == other.umur and
self.email == other.email)
def __hash__(self):
return hash((self.nama, self.umur, self.email))
# === Dataclass (singkat, otomatis!) ===
from dataclasses import dataclass
@dataclass
class Pengguna:
nama: str
umur: int
email: str
# Keduanya menghasilkan perilaku yang sama!
p1 = Pengguna("Budi", 25, "budi@mail.com")
p2 = Pengguna("Budi", 25, "budi@mail.com")
print(p1) # Pengguna(nama='Budi', umur=25, email='budi@mail.com')
print(p1 == p2) # True
print(repr(p1)) # Pengguna(nama='Budi', umur=25, email='budi@mail.com')
# Dataclass menghemat ~15 baris kode per class!
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β @dataclass AUTO-GENERATED METHODS β β β β Deklarasi: Method yang dihasilkan: β β ββββββββββββ ββββββββββββββββββββββββ β β β @dataclassβ β β __init__() β β β β class Foo:ββββββββΊβ β __repr__() β β β β x: int β β β __eq__() β β β β y: str β β β __hash__() β β β ββββββββββββ β β __lt__(), dll β β β ββββββββββββββββββββββββ β β β β order=True β β __lt__, __le__, __gt__, __ge__ β β frozen=True β β __setattr__, __delattr__ β β eq=False β β __eq__ (pakai default id) β β unsafe_hash=True β β __hash__ (meskipun mutable) β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2. Sintaks Dasar @dataclass
from dataclasses import dataclass, field
# === Dataclass paling sederhana ===
@dataclass
class Titik:
x: float
y: float
t = Titik(3.0, 4.0)
print(t) # Titik(x=3.0, y=4.0)
print(t.x, t.y) # 3.0 4.0
# === Dataclass dengan default values ===
@dataclass
class Produk:
nama: str
harga: float
stok: int = 0 # Default 0
aktif: bool = True # Default True
kategori: str = "umum" # Default "umum"
p1 = Produk("Laptop", 15000000)
p2 = Produk("Mouse", 150000, stok=50, kategori="aksesoris")
print(p1) # Produk(nama='Laptop', harga=15000000, stok=0, aktif=True, kategori='umum')
print(p2) # Produk(nama='Mouse', harga=150000, stok=50, aktif=True, kategori='aksesoris')
# === Dataclass dengan berbagai tipe ===
from typing import Optional
@dataclass
class Mahasiswa:
nama: str
nim: str
jurusan: str
semester: int = 1
ipk: Optional[float] = None
aktif: bool = True
mhs = Mahasiswa("Budi Santoso", "2024001", "Teknik Informatika")
print(mhs)
# Mahasiswa(nama='Budi Santoso', nim='2024001', jurusan='Teknik Informatika',
# semester=1, ipk=None, aktif=True)
# === Dataclass dengan method tambahan ===
@dataclass
class Lingkaran:
jari_jari: float
@property
def luas(self) -> float:
import math
return math.pi * self.jari_jari ** 2
@property
def keliling(self) -> float:
import math
return 2 * math.pi * self.jari_jari
def scale(self, faktor: float) -> 'Lingkaran':
return Lingkaran(self.jari_jari * faktor)
l = Lingkaran(5)
print(f"Luas: {l.luas:.2f}") # Luas: 78.54
print(f"Keliling: {l.keliling:.2f}") # Keliling: 31.42
print(l.scale(2)) # Lingkaran(jari_jari=10)
Decorator Parameters
from dataclasses import dataclass
# @dataclass bisa menerima parameter:
# init=True β generate __init__ (default)
# repr=True β generate __repr__ (default)
# eq=True β generate __eq__ (default)
# order=False β generate __lt__, __le__, __gt__, __ge__
# frozen=False β buat immutable
# unsafe_hash=False β generate __hash__
# Tanpa __init__ (manual)
@dataclass(init=False)
class CustomInit:
x: int
y: int
def __init__(self, x: int, y: int = 0):
self.x = x
self.y = y + 10 # Custom logic
c = CustomInit(5)
print(c) # CustomInit(x=5, y=10)
# Dengan ordering
@dataclass(order=True)
class Siswa:
nama: str
nilai: float
siswa_list = [
Siswa("Budi", 85),
Siswa("Ani", 92),
Siswa("Dimas", 78),
]
# Bisa di-sort langsung!
siswa_list.sort(reverse=True)
for s in siswa_list:
print(f" {s.nama}: {s.nilai}")
# Ani: 92
# Budi: 85
# Dimas: 78
# Tanpa __eq__ (gunakan default identity comparison)
@dataclass(eq=False)
class NoEquality:
id: int
nama: str
a = NoEquality(1, "test")
b = NoEquality(1, "test")
print(a == b) # False β berbeda objek, eq=False
3. Field Options
Fungsi field() memberikan kontrol lebih detail pada setiap field, seperti default value yang mutable, factory, visibility, dan opsi comparison.
from dataclasses import dataclass, field
# === Default factory untuk mutable defaults ===
# β SALAH β mutable default akan di-share antar instance!
# @dataclass
# class Config:
# tags: list[str] = [] β ERROR: ValueError!
# β
BENAR β gunakan field(default_factory=...)
@dataclass
class Config:
tags: list[str] = field(default_factory=list)
metadata: dict = field(default_factory=dict)
warnings: list[str] = field(default_factory=list)
c1 = Config()
c2 = Config()
c1.tags.append("python")
print(c1.tags) # ['python']
print(c2.tags) # [] β tidak terpengaruh!
# === field() parameters ===
# default β nilai default
# default_factory β callable untuk default mutable
# repr β tampilkan di __repr__?
# compare β gunakan di __eq__?
# hash β gunakan di __hash__?
# init β sertakan di __init__?
# metadata β dict metadata tambahan
@dataclass
class User:
nama: str
email: str
password_hash: str = field(repr=False) # Sembunyikan dari repr
api_key: str = field(compare=False) # Abaikan saat perbandingan
login_count: int = field(default=0, init=False) # Tidak di __init__
_internal: str = field(default="secret", repr=False, compare=False, hash=False)
u = User("Budi", "budi@mail.com", "hashed_pw_123")
print(u)
# User(nama='Budi', email='budi@mail.com') β password tidak tampil!
# === field dengan metadata ===
@dataclass
class ValidatedField:
nama: str = field(metadata={"max_length": 50, "pattern": r"^[a-zA-Z ]+$"})
umur: int = field(metadata={"min": 0, "max": 150})
email: str = field(metadata={"format": "email"})
# Metadata bisa diakses via fields()
from dataclasses import fields
for f in fields(ValidatedField):
print(f"{f.name}: {f.metadata}")
# nama: {'max_length': 50, 'pattern': '^[a-zA-Z ]+$'}
# umur: {'min': 0, 'max': 150}
# email: {'format': 'email'}
Default Values dan Required Fields
from dataclasses import dataclass, field
# Aturan: field tanpa default HARUS ada SEBELUM field dengan default
# β SALAH
# @dataclass
# class Bad:
# nama: str = "default"
# umur: int β ERROR! Non-default after default
# β
BENAR
@dataclass
class Good:
umur: int # Required (tanpa default)
nama: str # Required (tanpa default)
kota: str = "Jakarta" # Optional (dengan default)
aktif: bool = True # Optional (dengan default)
# Membuat field "required" meskipun ada default_factory
# dengan menggunakan sentinel pattern
_MISSING = object()
@dataclass
class FlexibleConfig:
host: str
port: int = field(default=8080)
debug: bool = field(default=False)
custom_header: str = field(default_factory=lambda: _MISSING)
def __post_init__(self):
if self.custom_header is _MISSING:
self.custom_header = f"Server-{self.host}"
c = FlexibleConfig("localhost")
print(c.custom_header) # Server-localhost
4. Post-Init Processing (__post_init__)
Method __post_init__ dipanggil setelah __init__ selesai. Ini sangat berguna untuk validasi data, menghitung derived fields, atau operasi setup tambahan.
from dataclasses import dataclass, field
# === Validasi data ===
@dataclass
class UserProfile:
nama: str
umur: int
email: str
def __post_init__(self):
# Validasi nama
if not self.nama or len(self.nama.strip()) == 0:
raise ValueError("Nama tidak boleh kosong!")
self.nama = self.nama.strip().title()
# Validasi umur
if self.umur < 0 or self.umur > 150:
raise ValueError(f"Umur tidak valid: {self.umur}")
# Validasi email
if "@" not in self.email:
raise ValueError(f"Email tidak valid: {self.email}")
self.email = self.email.lower()
# Test validasi
user = UserProfile(" budi santoso ", 25, "BUDI@Mail.com")
print(user)
# UserProfile(nama='Budi Santoso', umur=25, email='budi@mail.com')
# UserProfile("", 25, "test@mail.com") β ValueError!
# UserProfile("Budi", -5, "test@mail.com") β ValueError!
# === Derived/Computed fields ===
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False) # Tidak di __init__, dihitung otomatis
perimeter: float = field(init=False) # Sama
def __post_init__(self):
self.area = self.width * self.height
self.perimeter = 2 * (self.width + self.height)
r = Rectangle(10, 5)
print(f"Luas: {r.area}") # Luas: 50
print(f"Keliling: {r.perimeter}") # Keliling: 30
# === InitVar β variable khusus untuk __post_init__ ===
from dataclasses import dataclass, field, InitVar
@dataclass
class User:
nama: str
email: str
password: InitVar[str] = None # Hanya di __post_init__, bukan field
confirm_password: InitVar[str] = None
password_hash: str = field(init=False, repr=False)
def __post_init__(self, password: str, confirm_password: str):
if password != confirm_password:
raise ValueError("Password tidak cocok!")
self.password_hash = f"hashed_{password}"
u = User("Budi", "budi@mail.com", "rahasia123", "rahasia123")
print(u) # User(nama='Budi', email='budi@mail.com')
print(u.password_hash) # hashed_rahasia123
# 'password' bukan attribute β hanya digunakan saat init!
# === Validasi dengan kombinasi field ===
@dataclass
class DateRange:
start: str
end: str
def __post_init__(self):
from datetime import datetime
fmt = "%Y-%m-%d"
d_start = datetime.strptime(self.start, fmt)
d_end = datetime.strptime(self.end, fmt)
if d_end < d_start:
raise ValueError(f"End date ({self.end}) sebelum start date ({self.start})")
dr = DateRange("2026-01-01", "2026-12-31") # β
OK
# DateRange("2026-12-31", "2026-01-01") β ValueError!
5. Frozen Dataclasses
Frozen dataclass adalah dataclass yang immutable β setelah dibuat, field-nya tidak bisa diubah. Ini sangat berguna untuk data konfigurasi, constants, dan data yang perlu menjadi hashable.
from dataclasses import dataclass, field
# === Frozen dataclass β immutable ===
@dataclass(frozen=True)
class Color:
r: int
g: int
b: int
merah = Color(255, 0, 0)
hijau = Color(0, 255, 0)
print(merah) # Color(r=255, g=0, b=0)
# β Tidak bisa diubah!
# merah.r = 128 β FrozenInstanceError!
# β
Bisa jadi key dictionary (karena hashable)
color_names = {
merah: "Merah",
hijau: "Hijau",
}
print(color_names[merah]) # Merah
# β
Bisa jadi elemen set
warna_set = {merah, hijau, Color(0, 0, 255)}
# === Frozen dengan field yang perlu mutable ===
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
# Gunakan tuple, bukan list (karena frozen)
allowed_origins: tuple[str, ...] = ("https://example.com",)
config = Config("localhost", 3000, ("http://localhost:3000",))
print(config)
# Config(host='localhost', port=3000, allowed_origins=('http://localhost:3000',))
# === Membuat "copy" dengan perubahan (replace) ===
from dataclasses import replace
config_baru = replace(config, port=8080, host="0.0.0.0")
print(config_baru)
# Config(host='0.0.0.0', port=8080, allowed_origins=('http://localhost:3000',))
print(config) # Tetap tidak berubah!
# === Konstanta dengan frozen dataclass ===
@dataclass(frozen=True)
class APIConfig:
BASE_URL: str = "https://api.example.com"
VERSION: str = "v2"
TIMEOUT: int = 30
MAX_RETRIES: int = 3
@property
def full_url(self) -> str:
return f"{self.BASE_URL}/{self.VERSION}"
api = APIConfig()
print(api.full_url) # https://api.example.com/v2
Gunakan frozen=True untuk data yang sehariknya tidak berubah β konfigurasi, constants, data dari API response, atau sebagai key dictionary. Ini membantu mencegah bugs akibat perubahan data yang tidak disengaja.
6. Inheritance
Dataclass mendukung inheritance dengan baik, termasuk kombinasi default values dan field dari parent class.
from dataclasses import dataclass, field
# === Basic inheritance ===
@dataclass
class Hewan:
nama: str
umur: int
spesies: str
@dataclass
class HewanPeliharaan(Hewan):
nama_pemilik: str
sudah_vaksin: bool = False
kucing = HewanPeliharaan("Kitty", 3, "Kucing", "Budi", True)
print(kucing)
# HewanPeliharaan(nama='Kitty', umur=3, spesies='Kucing',
# nama_pemilik='Budi', sudah_vaksin=True)
# Akses field dari parent
print(f"{kucing.nama} ({kucing.spesies})") # Kitty (Kucing)
# === Multi-level inheritance ===
@dataclass
class Base:
id: int
created_at: str = "2026-01-01"
@dataclass
class Middle(Base):
name: str
status: str = "active"
@dataclass
class Leaf(Middle):
extra: float = 0.0
obj = Leaf(id=1, name="test", status="pending", extra=3.14)
print(obj)
# Leaf(id=1, created_at='2026-01-01', name='test', status='pending', extra=3.14)
# === Inheritance dengan override ===
@dataclass
class Shape:
name: str
color: str = "hitam"
def describe(self) -> str:
return f"{self.color} {self.name}"
@dataclass
class Circle(Shape):
radius: float = 1.0
name: str = "Lingkaran" # Override default dari parent
@property
def area(self) -> float:
import math
return math.pi * self.radius ** 2
c = Circle(radius=5)
print(c.describe()) # hitam Lingkaran
print(f"Luas: {c.area:.2f}") # Luas: 78.54
# === Abstract base dengan dataclass ===
from abc import ABC, abstractmethod
@dataclass
class BaseEntity(ABC):
id: int
nama: str
@abstractmethod
def validate(self) -> bool:
pass
@dataclass
class Product(BaseEntity):
harga: float
stok: int = 0
def validate(self) -> bool:
return self.harga > 0 and self.stok >= 0
@dataclass
class Service(BaseEntity):
durasi_menit: int
harga_per_menit: float
def validate(self) -> bool:
return self.durasi_menit > 0
@property
def total_harga(self) -> float:
return self.durasi_menit * self.harga_per_menit
p = Product(1, "Laptop", 15000000, 10)
s = Service(2, "Konsultasi", 60, 50000)
print(f"{p.nama}: Rp {p.harga:,.0f} (valid={p.validate()})")
print(f"{s.nama}: Rp {s.total_harga:,.0f} (valid={s.validate()})")
Dalam dataclass inheritance, field tanpa default di child class harus ditempatkan setelah field dengan default dari parent. Jika tidak, Python akan raise TypeError. Ini berbeda dari regular class karena dataclass generate __init__ secara otomatis.
7. Slots & Performa
from dataclasses import dataclass
import sys
# === Tanpa slots (default) ===
@dataclass
class TanpaSlots:
x: int
y: int
z: int
# === Dengan slots (Python 3.10+) ===
@dataclass(slots=True)
class DenganSlots:
x: int
y: int
z: int
a = TanpaSlots(1, 2, 3)
b = DenganSlots(1, 2, 3)
# Slots menghemat memori
print(f"Tanpa slots: {sys.getsizeof(a)} bytes") # ~48 bytes
print(f"Dengan slots: {sys.getsizeof(b)} bytes") # ~40 bytes
# Slots juga mempercepat akses attribute
import timeit
t1 = timeit.timeit(lambda: a.x, number=1000000)
t2 = timeit.timeit(lambda: b.x, number=1000000)
print(f"Tanpa slots: {t1:.4f}s")
print(f"Dengan slots: {t2:.4f}s")
# Slots biasanya ~20-30% lebih cepat!
# === Perbedaan behavior ===
# Tanpa slots β bisa tambah attribute baru
a.w = 99 # β
OK
# Dengan slots β tidak bisa tambah attribute baru
# b.w = 99 β AttributeError!
# === Slots + Frozen ===
@dataclass(frozen=True, slots=True)
class ImmutablePoint:
x: float
y: float
def distance_to(self, other: 'ImmutablePoint') -> float:
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5
p1 = ImmutablePoint(0, 0)
p2 = ImmutablePoint(3, 4)
print(f"Jarak: {p1.distance_to(p2)}") # Jarak: 5.0
8. Perbandingan dengan Alternatif
| Fitur | @dataclass | NamedTuple | Pydantic |
|---|---|---|---|
| Mutable | β Ya (opsional frozen) | β Immutable | β Ya |
| Auto __init__ | β | β | β |
| Auto __repr__ | β | β | β |
| Auto __eq__ | β | β | β |
| Validation | β Manual | β Manual | β Built-in |
| Serialization | β Manual | β Manual | β JSON, dict |
| Inheritance | β | β οΈ Terbatas | β |
| Slots | β (3.10+) | β Default | β (2.0+) |
| Library | stdlib | stdlib | pip install |
| Performa | π’ Cepat | π’ Cepat | π‘ Sedang |
# === NamedTuple ===
from typing import NamedTuple
class PointNT(NamedTuple):
x: float
y: float
p = PointNT(3.0, 4.0)
# p.x = 5.0 β Error! Immutable
# === Dict-like dataclass (TypedDict) ===
from typing import TypedDict
class PointDict(TypedDict):
x: float
y: float
d: PointDict = {"x": 3.0, "y": 4.0}
# === Kapan pakai yang mana? ===
#
# @dataclass:
# β
Data yang bisa berubah (mutable)
# β
Perlu method tambahan
# β
Inheritance kompleks
# β
Domain model, service objects
#
# NamedTuple:
# β
Data ringkas dan immutable
# β
Bisa jadi tuple (unpacking, indexing)
# β
Return value dari fungsi
#
# Pydantic:
# β
Perlu validasi otomatis
# β
API request/response models
# β
Serialization (JSON) built-in
# β
Data dari sumber eksternal
9. Studi Kasus Praktis
E-Commerce Models
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
@dataclass
class Product:
id: int
nama: str
harga: float
stok: int = 0
kategori: str = "umum"
def __post_init__(self):
if self.harga < 0:
raise ValueError(f"Harga tidak boleh negatif: {self.harga}")
if self.stok < 0:
raise ValueError(f"Stok tidak boleh negatif: {self.stok}")
def is_available(self, jumlah: int = 1) -> bool:
return self.stok >= jumlah
def reduce_stock(self, jumlah: int) -> None:
if not self.is_available(jumlah):
raise ValueError(f"Stok tidak cukup: {self.stok} < {jumlah}")
self.stok -= jumlah
@dataclass
class OrderItem:
produk: Product
jumlah: int
harga_satuan: float = field(init=False)
def __post_init__(self):
self.harga_satuan = self.produk.harga
@property
def subtotal(self) -> float:
return self.harga_satuan * self.jumlah
@dataclass
class Order:
id: str
items: list[OrderItem] = field(default_factory=list)
status: OrderStatus = OrderStatus.PENDING
created_at: datetime = field(default_factory=datetime.now)
catatan: Optional[str] = None
@property
def total(self) -> float:
return sum(item.subtotal for item in self.items)
@property
def jumlah_item(self) -> int:
return sum(item.jumlah for item in self.items)
def add_item(self, produk: Product, jumlah: int) -> None:
if not produk.is_available(jumlah):
raise ValueError(f"Stok {produk.nama} tidak cukup!")
self.items.append(OrderItem(produk, jumlah))
def checkout(self) -> float:
if not self.items:
raise ValueError("Order kosong!")
for item in self.items:
item.produk.reduce_stock(item.jumlah)
self.status = OrderStatus.PAID
return self.total
# === Penggunaan ===
laptop = Product(1, "Laptop Gaming", 15000000, stok=10)
mouse = Product(2, "Mouse Wireless", 150000, stok=50)
keyboard = Product(3, "Mechanical Keyboard", 350000, stok=30)
order = Order("ORD-001")
order.add_item(laptop, 1)
order.add_item(mouse, 2)
order.add_item(keyboard, 1)
print(f"Order: {order.id}")
print(f"Jumlah item: {order.jumlah_item}")
for item in order.items:
print(f" - {item.produk.nama} x{item.jumlah} = Rp {item.subtotal:,.0f}")
print(f"Total: Rp {order.total:,.0f}")
print(f"Status: {order.status.value}")
total = order.checkout()
print(f"\nβ
Checkout berhasil! Total: Rp {total:,.0f}")
print(f"Laptop stok tersisa: {laptop.stok}")
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Dataclasses: