Bug paling menyebalkan biasanya bukan soal logika yang keliru, tapi tipe data yang tidak cocok. Fungsi kamu mengharapkan int, dipanggil dengan str. Variabel yang kamu kira selalu berisi ternyata None di satu cabang. Python tidak peduli saat kode ditulis — dia baru melempar TypeError atau AttributeError ketika baris itu benar-benar dieksekusi, dan seringnya itu terjadi di produksi, bukan di laptop kamu. mypy adalah alat yang membaca kode Python tanpa menjalankannya, menebak tipe setiap variabel, lalu melaporkan ketidakcocokan sebelum runtime. Yang kamu butuhkan cuma Python 3.8 ke atas dan satu file untuk dicoba.
Type hints saja tidak cukup
Sejak Python 3.5, kamu bisa menulis anotasi tipe langsung di tanda tangan fungsi. Contoh kecil:
def total_harga(items: list[dict]) -> float:
return sum(item["harga"] for item in items)
Sekilas aman. Tapi panggil dengan data seperti ini:
total_harga([{"harga": 15000}, {"nama": "Buku"}])
Baris itu meledak dengan KeyError: 'harga' saat dijalankan. Anotasi list[dict] tidak menyelamatkan apa pun, karena Python menyimpan anotasi sebagai metadata di __annotations__ dan tidak pernah memeriksanya. Kalau kamu pernah menulis TypeScript di JavaScript, konsepnya mirip dengan static typing di TypeScript — bedanya, di Python pengecekannya ditambahkan lewat alat eksternal seperti mypy, bukan bagian bawaan bahasa.
Instal mypy
Pasang lewat pip:
pip install mypy
Kalau proyek kamu pakai uv, tambahkan sebagai dev dependency supaya tidak ikut terbawa ke produksi:
uv add --dev mypy
Menjalankan mypy pertama kali
Simpan dua potongan kode total_harga tadi ke berkas store.py, lalu jalankan:
mypy store.py
Hasilnya: Success: no issues found in 1 source file. Kok lolos? Karena list[dict] terlalu longgar — dict bisa berisi apa saja, termasuk objek tanpa kunci harga. mypy hanya bisa memeriksa apa yang bisa dibuktikan dari anotasi yang kamu beri. Makin spesifik anotasinya, makin tajam laporannya.
Anotasi dasar yang wajib kamu kuasai
Mulai dari tipe bawaan, lalu gabungkan untuk koleksi:
def bagi(a: int, b: int) -> float:
return a / b
def sapa(nama: str) -> str:
return "Halo, " + nama
def jumlahkan(data: dict[str, int]) -> int:
return sum(data.values())
# dua baris yang salah tipe
sapa(123)
jumlahkan({"a": "bukan angka"})
Jalankan mypy dan kamu dapat laporan begini:
store.py:10: error: Argument 1 to "sapa" has incompatible type "int"; expected "str" [arg-type]
store.py:11: error: Dict entry 1 has incompatible type "str": "str"; expected "str": "int" [dict-item]
Setiap error punya kode di akhir dalam kurung siku — arg-type, dict-item — yang nanti berguna untuk menonaktifkan error secara selektif. Untuk nilai yang boleh kosong, pakai tanda pipa:
def cari_user(nama: str) -> dict[str, str] | None:
if nama == "admin":
return {"nama": "Admin", "role": "superuser"}
return None
Biasakan menangani None. mypy akan menolak kode yang membaca atribut dari nilai bertipe X | None tanpa guard if x is None. Itu bukan rewel — error 'NoneType' object has no attribute ... adalah salah satu crash paling umum di Python.
TypedDict dan Protocol untuk tipe yang lebih presisi
Kembali ke contoh total_harga. Daripada list[dict], gunakan TypedDict supaya mypy tahu persis kunci apa yang wajib ada:
from typing import TypedDict
class Item(TypedDict):
harga: int
nama: str
def total_harga(items: list[Item]) -> int:
return sum(item["harga"] for item in items)
total_harga([{"harga": 15000, "nama": "Buku"}]) # lolos
total_harga([{"nama": "Buku"}]) # error: Missing key "harga" [typeddict-item]
Untuk duck typing — fungsi yang hanya peduli pada sebagian antarmuka objek — ada Protocol:
from typing import Protocol
class PunyaNama(Protocol):
nama: str
def cetak(obj: PunyaNama) -> None:
print(obj.nama)
Protocol membuat mypy menerima objek apa pun selama punya atribut nama, tanpa mewajibkan pewarisan. Kalau kebutuhanmu bukan sekadar cek statis, tapi validasi data saat runtime juga, arahkan ke Pydantic — keduanya bisa dipakai berdampingan: mypy untuk cek sebelum kode jalan, Pydantic untuk memvalidasi input yang datang dari luar.
Konfigurasi mypy lewat pyproject.toml
Setiap kali memanggil mypy tanpa argumen, ia membaca konfigurasi di pyproject.toml. Mulai dari sini:
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true
Mode strict menyalakan banyak flag sekaligus, termasuk disallow_untyped_defs (setiap fungsi wajib punya anotasi) dan check_untyped_defs. Untuk kode lama yang belum siap, turunkan standar per modul:
[[tool.mypy.overrides]]
module = ["legacy.*"]
disallow_untyped_defs = false
Gradual typing untuk kode lama
Tidak realistis memaksa seluruh proyek lolos strict dalam sehari. mypy dirancang untuk gradual typing: biarkan fungsi lama tanpa anotasi, anotasi yang baru kamu ubah. Untuk melihat tipe yang ditebak mypy pada satu variabel, pakai reveal_type():
def hitung(x):
reveal_type(x)
return x * 2
Jalankan mypy dan ia mencetak tipe yang berhasil disimpulkan — cara tercepat memahami kenapa sebuah baris dianggap bermasalah.
Jalankan mypy di CI
Cek tipe cuma berguna kalau dijalankan terus-menerus, bukan sekali sebulan. Di GitHub Actions, cukup begini:
name: type-check
on: [push, pull_request]
jobs:
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install mypy
- run: mypy .
Kalau kamu belum punya pipeline, tutorial GitHub Actions dari nol akan membantu. Setelah CI hijau, pertimbangkan juga menambah mypy ke pre-commit supaya cek berjalan setiap commit.
Error umum dan cara membacanya
Salah satu error yang paling sering muncul adalah union-attr, yaitu membaca atribut dari nilai yang mungkin None:
def nama_lengkap(user: dict[str, str] | None) -> str:
return user["nama"] + " " + user["belakang"] # error: union-attr
def nama_lengkap(user: dict[str, str] | None) -> str:
if user is None:
return "Tanpa Nama"
return user["nama"] + " " + user["belakang"]
Jangan menebar # type: ignore sembarangan. Any dan # type: ignore menghentikan pengecekan. Kalau benar-benar terpaksa, sertakan kode error-nya supaya niatmu jelas dan bisa diaudit: # type: ignore[arg-type]. Abaikan tanpa alasan hanya menutupi bug asli.
Jalankan mypy . di proyek kamu sekarang. Kalau hasilnya puluhan error, itu normal untuk kode lama. Bereskan dari berkas yang paling sering berubah, lalu kunci dengan strict = true di pyproject.toml. Begitu cek tipe hijau, tambahkan ke CI — biarkan mesin yang menjaga tipe, bukan ingatanmu.