Section Type Hints dan Static Analysis sudah membahas sintaks anotasi secara menyeluruh — dari list[float] sampai generics dan Protocol. Chapter ini mengambil sudut pandang yang berbeda: bukan bagaimana menulis type hints, melainkan mengapa anotasi menjadi penjaga jangka panjang. Kode yang hidup terus berubah: signature diubah, data berpindah bentuk, fitur ditambah. Tanpa kontrak yang diperiksa otomatis, setiap perubahan besar jadi tebak-tebakan; dengan mypy, perubahan yang merusak pemanggil lama tertangkap sebelum kode sempat berjalan.
Dokumentasi yang Tidak Boleh Lapuk #
Docstring menjanjikan "jumlah: float", tetapi janji itu tidak dijaga siapa pun — interpreter Python mengabaikan anotasi saat runtime. Ketika pemanggil mengirim string, program baru gagat di tengah jalan. Type hints memindahkan janji itu ke tempat yang diperiksa static type checker setiap hari:
| Aspek | Docstring | Type hints + mypy | Pemeriksaan runtime |
|---|---|---|---|
| Menjelaskan maksud | ya, bebas narasi | ya, terstruktur | tidak |
| Diverifikasi otomatis | tidak | ya, tiap kali mypy dijalankan | ya, saat baris dieksekusi |
| Menangkap pemanggilan salah | tidak | sebelum program berjalan | saat crash |
| Lapuk seiring waktu | bisa | tidak — anotasi menempel pada signature | — |
Keduanya bukan pesaing: docstring menjelaskan makna, type hints mengunci bentuk. Kontrak data paling nyata terlihat pada dataclass — simpan sebagai file tagihan.py:
"""Struktur data tagihan sebagai kontrak."""
from dataclasses import dataclass
@dataclass
class Tagihan:
id_tagihan: str
jumlah: float
sudah_bayar: bool
def total_terbayar(tagihan: list[Tagihan]) -> float:
"""Jumlahkan semua tagihan yang sudah dibayar."""
return sum(t.jumlah for t in tagihan if t.sudah_bayar)
daftar = [
Tagihan("INV-001", 150_000, True),
Tagihan("INV-002", 200_000, False),
]
print(total_terbayar(daftar))Output yang Dihasilkan:
150000Class Tagihan kini punya tiga field bertipe tetap. Lihat apa terjadi bila pemanggil mengirim string pada field jumlah:
"""Pemanggil yang melanggar kontrak."""
from tagihan import Tagihan, total_terbayar
daftar = [
Tagihan(id_tagihan="INV-001", jumlah="seratus ribu", sudah_bayar=True),
]
print(total_terbayar(daftar))python3 -m mypy salah_pakai.pyOutput yang Dihasilkan:
salah_pakai.py:6: error: Argument "jumlah" to "Tagihan" has incompatible type "str"; expected "float" [arg-type]
Found 1 error in 1 file (checked 1 source file)mypy menolak kode ini sebelum dijalankan — tanpa anotasi, program jalan sampai sum() menabrak TypeError di production. Untuk data berbentuk dictionary yang fieldnya diketik longgar, TypedDict berperan sama: setiap key dan value diperiksa sebagai kontrak.
Demo: Refactor Aman dengan mypy #
Skenario paling umum dalam maintenance: mengubah signature sebuah function. Bayangkan package toko/ dengan dua file — keranjang.py dan laporan.py. Isi keranjang.py:
"""Perhitungan keranjang belanja."""
def total_keranjang(harga_satuan: list[float], pajak: float) -> float:
"""Hitung total keranjang termasuk pajak."""
subtotal = sum(harga_satuan)
return subtotal * (1 + pajak)Sementara laporan.py memanggilnya untuk mencetak laporan:
"""Cetak laporan penjualan."""
from keranjang import total_keranjang
print(total_keranjang([5000, 12000], 0.11))Langkah 1 — pastikan kondisi awal sehat (dijalankan dari dalam folder toko/):
python3 -m mypy .Output yang Dihasilkan:
Success: no issues found in 2 source filesLangkah 2 — kebutuhan berubah: keranjang kini menyimpan object Item, bukan angka polos. Ganti seluruh isi keranjang.py:
"""Perhitungan keranjang belanja."""
from dataclasses import dataclass
@dataclass
class Item:
nama: str
harga: float
def total_keranjang(items: list[Item], pajak: float) -> float:
"""Hitung total keranjang termasuk pajak."""
subtotal = sum(item.harga for item in items)
return subtotal * (1 + pajak)Jalankan mypy sekali lagi:
laporan.py:5: error: List item 0 has incompatible type "int"; expected "Item" [list-item]
laporan.py:5: error: List item 1 has incompatible type "int"; expected "Item" [list-item]
Found 2 errors in 1 file (checked 2 source files)mypy menunjuk tepat ke laporan.py baris 5 — pemanggil lama yang masih mengirim int. Tanpa anotasi, perubahan ini lolos review dan baru meledak saat laporan dicetak.
Langkah 3 — perbaiki laporan.py agar membangun Item, lalu jalankan ulang:
"""Cetak laporan penjualan."""
from keranjang import Item, total_keranjang
keranjang = [
Item("Kopi sachet", 5000),
Item("Gula 1 kg", 12000),
]
print(total_keranjang(keranjang, 0.11))Output yang Dihasilkan (python3 laporan.py, lalu python3 -m mypy .):
18870.0
Success: no issues found in 2 source filesInilah lingkaran kerja yang membuat refactor jadi aman: ubah satu hal, jalankan mypy, biarkan tool mendaftar semua lokasi yang wajib menyesuaikan.
Migrasi Bertahap pada Kode Lama #
Menambahkan anotasi pada proyek berisi ratusan file tidak bisa sekaligus. Strateginya meniru pola strangler: mulai dari public API — boundary tempat pemanggil luar menyentuh kode Anda — karena di sanalah kontrak paling berharga. Function privat yang hanya dipanggil satu tempat bisa menunggu.
mypy mendukung pendekatan ini lewat strictness per module. Simpan mypy.ini di root proyek:
[mypy]
[mypy-tagihan.*]
disallow_untyped_defs = TrueBagian [mypy-tagihan.*] menerapkan aturan ketat hanya pada package tagihan; sisanya diperiksa longgar. Misalkan tagihan.py sudah dianotasi penuh sementara migrasi.py — hasil migrasi yang belum selesai — masih memuat function tanpa anotasi: keduanya lolos. Sekarang seseorang menambahkan function baru sapa_pengguna(nama) tanpa anotasi pada tagihan.py baris 9; mypy langsung menolak:
tagihan.py:9: error: Function is missing a type annotation [no-untyped-def]
Found 1 error in 1 file (checked 2 source files)Function serupa di module lain tetap diterima — jejak migrasinya jelas: sekelompok file yang sudah dianotasi penuh, lalu satu per satu module baru dipindahkan ke zona ketat dengan menambah satu baris konfigurasi. Setiap kali zona itu melebar, kontrak yang dijaga mypy ikut melebar.