Buka proyek Python yang sudah kamu tinggalkan enam bulan. Kamu menemukan fungsi process(data, config) dan sama sekali lupa data itu list, dict, atau objek khusus. Type hints ada untuk mencegah momen seperti ini. Python tidak pernah mewajibkan kamu menulis tipe, dan runtime-nya tetap tidak peduli. Tapi begitu proyek lewat seribu baris, anotasi tipe mengubah kode dari teka-teki jadi dokumentasi yang bisa dibaca mesin.
Kita mulai dari yang paling dasar, lalu naik ke pola yang sering muncul di proyek sungguhan: Optional, generik, Protocol, dan TypedDict. Di ujung, kamu akan bisa menjalankan mypy untuk menangkap bug tipe sebelum kode berjalan.
Dasar Anotasi
Anotasi paling sederhana adalah memberi tahu tipe sebuah variabel atau parameter. Sintaksnya pakai titik dua, dan tipe kembalian fungsi pakai tanda panah ->.
nama: str = "Budi"
umur: int = 27
tinggi: float = 175.5
aktif: bool = True
def sapa(nama: str) -> str:
return f"Halo, {nama}"
def tambah(a: int, b: int) -> int:
return a + b
Perhatikan: anotasi ini tidak mengubah apa pun saat program berjalan. tambah("a", "b") tetap dieksekusi dan menghasilkan "ab" — Python tidak peduli. Nilai type hints baru terasa saat kamu pakai editor modern (VS Code, PyCharm) atau alat seperti mypy untuk memeriksa kode secara statis.
List, Dict, dan Generik Bawaan
Kalau kamu tulis list saja, editor tidak tahu isinya apa. Sejak Python 3.9 kamu bisa menulis tipe elemen di dalam kurung siku langsung, tanpa impor tambahan.
def rata_rata(nilai: list[float]) -> float:
return sum(nilai) / len(nilai)
def hitung_kata(kalimat: str) -> dict[str, int]:
hasil: dict[str, int] = {}
for kata in kalimat.split():
hasil[kata] = hasil.get(kata, 0) + 1
return hasil
koordinat: tuple[int, int] = (10, 20)
id_unik: set[int] = {1, 2, 3, 4}
Untuk Python 3.8 ke bawah, versi generiknya ada di modul typing: List[float], Dict[str, int], Tuple[int, int]. Kalau kamu masih mendukung versi lama, pakai itu. Mayoritas proyek baru sudah aman dengan sintaks kurung siku langsung.
Optional, Union, dan Operator |
Salah satu tipe yang paling sering kamu butuhkan adalah nilai yang bisa None. Pola lama memakai Optional[T], pola baru (Python 3.10+) cukup T | None. Dua-duanya valid, pilih satu dan konsisten di seluruh proyek.
def cari_pengguna(nama: str) -> dict | None:
database = {"budi": {"umur": 27}, "siti": {"umur": 31}}
return database.get(nama)
def bagi(a: int, b: int) -> float | None:
if b == 0:
return None
return a / b
def format_uang(jumlah: int | float) -> str:
return f"Rp {jumlah:,}"
Union biasa ditulis int | str. Hati-hati: kalau sebuah fungsi bisa melempar pengecualian, jangan dibungkus jadi | None — biarkan pengecualiannya naik. Tipe None khusus untuk nilai kosong yang memang valid secara bisnis.
Kalau kamu banyak memakai Optional dan list[...] dari modul typing, tambahkan from __future__ import annotations di baris pertama. Semua anotasi jadi string (lazy), jadi impor typing tidak dievaluasi saat runtime dan kode sedikit lebih cepat.
Type Alias
Ketika tipe yang sama muncul berulang — misalnya dict[str, int | float] untuk data sensor — beri nama. Alias membuat maksud kode langsung terbaca.
UserId = int
JsonData = dict[str, int | float | str | bool | None]
Koordinat = tuple[float, float]
def jarak(a: Koordinat, b: Koordinat) -> float:
dx = a[0] - b[0]
dy = a[1] - b[1]
return (dx * dx + dy * dy) ** 0.5
def simpan(payload: JsonData) -> None:
print(payload)
Sejak Python 3.12 ada kata kunci type yang lebih eksplisit: type UserId = int. Fungsinya sama, hanya lebih jelas bahwa kamu sedang mendefinisikan alias, bukan variabel. Saya tetap pakai tanda sama dengan biasa karena didukung lebih banyak versi.
Callable: Tipe untuk Fungsi
Kadang kamu menerima fungsi sebagai parameter — callback, handler, atau mapper. Anotasikan dengan Callable.
from collections.abc import Callable
def terapkan(operasi: Callable[[int, int], int], a: int, b: int) -> int:
return operasi(a, b)
def kali(x: int, y: int) -> int:
return x * y
print(terapkan(kali, 4, 5)) # 20
Argumen pertama Callable[[int, int], int] artinya: fungsi yang menerima dua int dan mengembalikan satu int. Impor dari collections.abc supaya kompatibel dengan isinstance.
Generik dengan TypeVar
Fungsi seperti terapkan kaku karena mengunci tipe int. Pakai TypeVar untuk membuat fungsi generik yang tetap menjaga hubungan tipe masuk-keluar.
from collections.abc import Sequence
from typing import TypeVar
T = TypeVar("T")
def pertama(daftar: Sequence[T]) -> T | None:
return daftar[0] if daftar else None
print(pertama([1, 2, 3])) # 1 (int)
print(pertama(["a", "b"])) # 'a' (str)
Tanpa TypeVar, kamu terpaksa mengembalikan Any atau menulis versi terpisah untuk setiap tipe. Dengan TypeVar, mypy tahu bahwa pertama([1, 2, 3]) mengembalikan int, bukan sembarang objek.
Any adalah jalan pintas yang menular. Sekali fungsi mengembalikan Any, semua kode yang memakainya kehilangan pemeriksaan tipe secara diam-diam. Pakai Any hanya saat berhadapan dengan kode dinamis (misalnya hasil json.loads), lalu segera sempitkan tipenya.
Protocol: Duck Typing yang Terdefinisi
Python menghargai duck typing — objek dianggap valid kalau punya metode yang dibutuhkan, bukan karena mewarisi kelas tertentu. Protocol memformalkan pola ini untuk mypy.
from typing import Protocol
class BisaBunyi(Protocol):
def bunyi(self) -> str: ...
class Kucing:
def bunyi(self) -> str:
return "meong"
class Alarm:
def bunyi(self) -> str:
return "bip bip"
def mainkan(objek: BisaBunyi) -> None:
print(objek.bunyi())
mainkan(Kucing()) # meong
mainkan(Alarm()) # bip bip
Perhatikan Kucing dan Alarm tidak mewarisi BisaBunyi. mypy tetap menerimanya karena keduanya memenuhi struktur yang diminta. Ini berbeda dari ABC yang memaksa pewarisan eksplisit.
TypedDict untuk Struktur Dict
Dict Python sering dipakai menyimpan data berstruktur — respons API, baris database, konfigurasi. TypedDict memberi tipe pada kunci-kuncinya tanpa mengubah runtime sama sekali.
from typing import TypedDict
class Pengguna(TypedDict):
nama: str
umur: int
email: str
def kirim_email(user: Pengguna) -> None:
print(f"Kirim ke {user['email']}")
budi: Pengguna = {"nama": "Budi", "umur": 27, "email": "budi@contoh.com"}
kirim_email(budi)
Kalau kamu typo saat mengakses kunci, misalnya user['emial'], mypy akan menangkapnya. Ini keuntungan besar dibanding dict polos. Untuk validasi runtime yang lebih ketat, Pydantic lebih cocok — saya tulis panduannya di artikel Menguasai Pydantic.
Literal, Final, dan NewType
Tiga alat kecil ini menutup celah yang sering lolos dari pengecekan biasa. Literal membatasi nilai, Final mencegah reassignment, NewType memisahkan tipe yang kebetulan sama.
from typing import Final, Literal, NewType
Mode = Literal["cepat", "hemat", "seimbang"]
def atur_mode(mode: Mode) -> None:
print(f"Mode aktif: {mode}")
atur_mode("cepat") # ok
# atur_mode("acak") # mypy error
MAX_RETRY: Final = 3
UserId = NewType("UserId", int)
OrderId = NewType("OrderId", int)
def proses(user: UserId) -> None:
print(user)
u = UserId(42)
proses(u)
# proses(42) # mypy error: int polos bukan UserId
NewType sering dianggap remeh, tapi mencegah bug nyata: tertukarnya user_id dan order_id yang sama-sama int. Di runtime, UserId benar-benar hanya int, jadi tidak ada overhead.
Menjalankan mypy
Semua anotasi di atas baru berguna kalau ada yang memeriksanya. Install mypy lalu jalankan.
pip install mypy
mypy program.py
Kalau tidak ada output error, semua tipe konsisten. mypy juga membaca pyproject.toml untuk konfigurasi — misalnya strict = true yang menyalakan pemeriksaan paling ketat. Cara pakai mypy secara mendalam, termasuk adopsi bertahap ke proyek lama, saya bahas di artikel terpisah.
Type Hints di FastAPI dan Pydantic
Kalau kamu membangun API, type hints bukan lagi opsional — dia adalah inti framework-nya. FastAPI membaca anotasi fungsi untuk membuat validasi otomatis dan dokumentasi OpenAPI.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
nama: str
harga: float
stok: int = 0
@app.post("/item")
def buat_item(item: Item) -> Item:
return item
Di sini Item memakai anotasi yang sama persis dengan yang kamu pelajari barusan — hanya saja Pydantic memakainya untuk validasi saat runtime. Kalau kamu belum kenal FastAPI, mulai dari tutorial dasar FastAPI.
Mulailah dari satu file: tambahkan anotasi pada fungsi publik, jalankan mypy, lalu perluas ke seluruh modul. Type hints yang ditulis bertahap jauh lebih baik daripada refactor besar yang tidak pernah selesai. Kalau kamu sering membuat struktur data, pasangkan anotasi ini dengan dataclasses — kombinasi keduanya menghilangkan sebagian besar boilerplate kelas.