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:

AspekDocstringType hints + mypyPemeriksaan runtime
Menjelaskan maksudya, bebas narasiya, terstrukturtidak
Diverifikasi otomatistidakya, tiap kali mypy dijalankanya, saat baris dieksekusi
Menangkap pemanggilan salahtidaksebelum program berjalansaat crash
Lapuk seiring waktubisatidak — 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:

python
"""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:

text
150000

Class Tagihan kini punya tiga field bertipe tetap. Lihat apa terjadi bila pemanggil mengirim string pada field jumlah:

python
"""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))
bash
python3 -m mypy salah_pakai.py

Output yang Dihasilkan:

text
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:

python
"""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:

python
"""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/):

bash
python3 -m mypy .

Output yang Dihasilkan:

text
Success: no issues found in 2 source files

Langkah 2 — kebutuhan berubah: keranjang kini menyimpan object Item, bukan angka polos. Ganti seluruh isi keranjang.py:

python
"""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:

text
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:

python
"""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 .):

text
18870.0
Success: no issues found in 2 source files

Inilah 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:

ini
[mypy]

[mypy-tagihan.*]
disallow_untyped_defs = True

Bagian [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:

text
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.